├── .autocorrectrc ├── .github └── workflows │ └── autocorrect.yml ├── README.md ├── cs_learn ├── README.md ├── cs_learn.md ├── feel_cs.md └── look_book.md ├── mysql ├── README.md ├── base │ ├── how_select.md │ └── row_format.md ├── buffer_pool │ ├── README.md │ └── buffer_pool.md ├── index │ ├── 2000w.md │ ├── count.md │ ├── index_interview.md │ ├── index_issue.md │ ├── index_lose.md │ ├── page.md │ └── why_index_chose_bpuls_tree.md ├── lock │ ├── deadlock.md │ ├── how_to_lock.md │ ├── lock_phantom.md │ ├── mysql_lock.md │ ├── show_lock.md │ └── update_index.md ├── log │ ├── README.md │ └── how_update.md └── transaction │ ├── mvcc.md │ └── phantom.md ├── network ├── 1_base │ ├── how_os_deal_network_package.md │ ├── tcp_ip_model.md │ └── what_happen_url.md ├── 2_http │ ├── http2.md │ ├── http3.md │ ├── http_interview.md │ ├── http_optimize.md │ ├── http_rpc.md │ ├── http_websocket.md │ ├── https_ecdhe.md │ ├── https_optimize.md │ └── https_rsa.md ├── 3_tcp │ ├── challenge_ack.md │ ├── isn_deff.md │ ├── out_of_order_fin.md │ ├── port.md │ ├── quic.md │ ├── syn_drop.md │ ├── tcp_down_and_crash.md │ ├── tcp_drop.md │ ├── tcp_feature.md │ ├── tcp_http_keepalive.md │ ├── tcp_interview.md │ ├── tcp_no_accpet.md │ ├── tcp_no_listen.md │ ├── tcp_optimize.md │ ├── tcp_problem.md │ ├── tcp_queue.md │ ├── tcp_stream.md │ ├── tcp_tcpdump.md │ ├── tcp_three_fin.md │ ├── tcp_tls.md │ ├── tcp_tw_reuse_close.md │ ├── tcp_unplug_the_network_cable.md │ └── time_wait_recv_syn.md ├── 4_ip │ ├── ip_base.md │ ├── ping.md │ └── ping_lo.md ├── 5_learn │ ├── draw.md │ └── learn_network.md └── README.md ├── os ├── 10_learn │ ├── draw.md │ └── learn_os.md ├── 1_hardware │ ├── cpu_mesi.md │ ├── float.md │ ├── how_cpu_deal_task.md │ ├── how_cpu_run.md │ ├── how_to_make_cpu_run_faster.md │ ├── soft_interrupt.md │ └── storage.md ├── 2_os_structure │ └── linux_vs_windows.md ├── 3_memory │ ├── alloc_mem.md │ ├── cache_lru.md │ ├── malloc.md │ ├── mem_reclaim.md │ └── vmem.md ├── 4_process │ ├── create_thread_max.md │ ├── deadlock.md │ ├── multithread_sync.md │ ├── pessim_and_optimi_lock.md │ ├── process_base.md │ ├── process_commu.md │ └── thread_crash.md ├── 5_schedule │ └── schedule.md ├── 6_file_system │ ├── file_system.md │ └── pagecache.md ├── 7_device │ └── device.md ├── 8_network_system │ ├── hash.md │ ├── reactor.md │ ├── selete_poll_epoll.md │ └── zero_copy.md ├── 9_linux_cmd │ ├── linux_network.md │ └── pv_uv.md └── README.md ├── reader_nb ├── 1_reader.md ├── 2_reader.md ├── 3_reader.md ├── 4_reader.md ├── 5_reader.md ├── 6_reader.md ├── 7_reader.md ├── 8_reader.md └── README.md └── redis ├── README.md ├── architecture └── mysql_redis_consistency.md ├── base └── redis_interview.md ├── cluster ├── cache_problem.md ├── cluster.md ├── master_slave_replication.md └── sentinel.md ├── data_struct ├── command.md └── data_struct.md ├── module └── strategy.md └── storage ├── aof.md ├── bigkey_aof_rdb.md └── rdb.md /.autocorrectrc: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://huacnlee.github.io/autocorrect/schema.json 2 | rules: 3 | # Default rules: https://github.com/huacnlee/autocorrect/raw/main/autocorrect/.autocorrectrc.default 4 | spellcheck: 2 5 | spellcheck: 6 | words: 7 | # Please do not add a general English word (eg. apple, python) here. 8 | # Users can add their special words to their .autocorrectrc file by their need. 9 | - ActiveMQ 10 | - AirPods 11 | - Aliyun 12 | - API 13 | - App Store 14 | - AppKit 15 | - AppStore = App Store 16 | - AWS 17 | - CacheStorage 18 | - CDN 19 | - CentOS 20 | - CloudFront 21 | - CORS 22 | - CPU 23 | - DNS 24 | - Elasticsearch 25 | - ESLint 26 | - Facebook 27 | - GeForce 28 | - GitHub 29 | - Google 30 | - GPU 31 | - H5 32 | - Hadoop 33 | - HBase 34 | - HDFS 35 | - HKEX 36 | - HTML 37 | - HTTP 38 | - HTTPS 39 | - I10n 40 | - I18n 41 | - iMovie 42 | - IndexedDB 43 | - Intel 44 | - iOS 45 | - iPad 46 | - iPadOS 47 | - iPhone 48 | - iTunes 49 | - JavaScript 50 | - jQuery 51 | - JSON 52 | - JWT 53 | - Linux 54 | - LocalStorage 55 | - macOS 56 | - Markdown 57 | - Microsoft 58 | - MongoDB 59 | - Mozilla 60 | - MVC 61 | - MySQL 62 | - Nasdaq 63 | - Netflix 64 | - NodeJS = Node.js 65 | - NoSQL 66 | - NVDIA 67 | - NYSE 68 | - OAuth 69 | - Objective-C 70 | - OLAP 71 | - OSS 72 | - P2P 73 | - PaaS 74 | - RabbitMQ 75 | - Redis 76 | - RESTful 77 | - RSS 78 | - RubyGem 79 | - RubyGems 80 | - SaaS 81 | - Sass 82 | - SDK 83 | - Shopify 84 | - SQL 85 | - SQLite 86 | - SQLServer 87 | - SSL 88 | - Tesla 89 | - TikTok 90 | - tvOS 91 | - TypeScript 92 | - Ubuntu 93 | - UML 94 | - URI 95 | - URL 96 | - VIM 97 | - watchOS 98 | - WebAssembly 99 | - WebKit 100 | - Webpack 101 | - Wi-Fi 102 | - Windows 103 | - WWDC 104 | - Xcode 105 | - XML 106 | - YAML 107 | - YML 108 | - YouTube 109 | -------------------------------------------------------------------------------- /.github/workflows/autocorrect.yml: -------------------------------------------------------------------------------- 1 | name: Autocorrect 2 | on: [push, pull_request] 3 | jobs: 4 | autocorrect: 5 | name: Check text autocorrect 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Check out source 9 | uses: actions/checkout@v3 10 | with: 11 | fetch-depth: 1 12 | 13 | - name: Exec autocorrect 14 | uses: huacnlee/autocorrect-action@main 15 | -------------------------------------------------------------------------------- /cs_learn/README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | 本系列是小林的个人的学习心得,希望对大家有启发:muscle:。 4 | 5 | - [学习计算机基础有什么推荐的书?](/cs_learn/cs_learn.md) 6 | - [看书的一点小建议](/cs_learn/look_book.md) 7 | - [如何将计算机网络、操作系统、数据结构与算法、计算组成融会贯通?](/cs_learn/feel_cs.md) 8 | 9 | -------------------------------------------------------------------------------- /cs_learn/look_book.md: -------------------------------------------------------------------------------- 1 | # 看书的一点小建议 2 | 3 | 大家好,我是小林。 4 | 5 | 昨天看到小北写了篇「[看书的一点小建议](https://mp.weixin.qq.com/s?__biz=Mzg4NjUxMzg5MA==&mid=2247490764&idx=1&sn=7f1b25efd659ee6ca66b4845fbdba9cb&scene=21#wechat_redirect)」,写的很不错,今天我也根据自己经历,分享下看计算机基础类书的心得。 6 | 7 | 每隔一段时间,都有些读者跑来请教我学习的心得。 8 | 9 | ![图片](https://img-blog.csdnimg.cn/img_convert/f3e678d6d722310ee4b5ea31b80abcd1.png) 10 | 11 | ![图片](https://img-blog.csdnimg.cn/img_convert/6a94b1c389461b1ba7ba16de2a45aaf6.png) 12 | 13 | 他们的困惑可以归类这几点: 14 | 15 | - 书看不懂,容易放弃,怎么办? 16 | - 看书的效率很低,怎么办? 17 | - 做了很多笔记,依然过会就忘记,怎么办? 18 | 19 | 这些困惑我曾经也经历过,中途也踩过很多坑,浪费了很多的时间,好在及时反思,调整了看书的方法,后面学习的效率立竿见影。 20 | 21 | ------ 22 | 23 | ## 不要直接选择困难模式 24 | 25 | 大家应该知道计算机书里有个黑皮系列的书,黑皮系列的书有一个共同的特点就是**厚重**! 26 | 27 | 我相信不少小伙伴在想要学习计算机基础类知识的时候,就买了这类黑皮书,书到货后,我们满怀信心,举着厚重的黑皮书,下决心要把这些黑皮书一页一页地攻读下来,结果不过几天就被劝退了,然后就只有前几页是有翻阅的痕迹,剩下的几百页都完全是新的,最终这些厚厚的黑皮书就成了**垫显示器的神器**。 28 | 29 | ![图片](https://img-blog.csdnimg.cn/img_convert/0293f9b6a101818b26b28e3464e9f567.png) 30 | 31 | 黑皮系列的书确实都是经典书,豆瓣评分都很高,知识点很全面,是好书无疑。但是这类书并不适合新手入门,你想想我们学习中文的时候,你是拿着新华字典学的吗?很显然不是。 32 | 33 | 黑皮书就好像游戏里「困难模式」,新人一上来就玩这个模式,根本就体会不到游戏的乐趣了,卸载了游戏那还是小事,如果留下心里阴影,造成不可逆的伤害,这就非常不好了。 34 | 35 | 说白了,这些厚的不行的计算机书不适合入门,我们应该先从「简单模式」慢慢过渡,**要屠龙,得先从新手村起步**。 36 | 37 | 就拿我亲身经历举例。 38 | 39 | 当初在学习计算机网络的时候,看见大家都说《计算机网络 - 自顶向下》和《TCP/IP 详解》这两本书好,我立马买了学习,这本也是黑皮系列大厚书,奈何小林当时太菜,根本就砍不动这本书,砍两下,刀钝了,就想睡觉。 40 | 41 | 后面又找了一波书,发现《图解 TCP/IP》、 《图解 HTTP》、《网络是怎么连接的》这几本书都不厚,而且搭配了很多图,我又立马买回来学习。 42 | 43 | 这几本书读起来不会太困难,不出一个月,我就把这三本书看完了,立马对计算机网络有了个整体且清晰的认识,终于知道了网络七层模型是什么,也知道了两台电脑是如何通过网络进行相互通信的,也知道 HTTP、DNS、TCP、UDP、IP、ICMP、DHCP、ARP 这些常见的协议是用来干嘛的了,成功突破了新手村。 44 | 45 | 虽然突破了新手村,但是学的知识还不够深入。 46 | 47 | 所以,我后面回来看《计算机网络 - 自顶向下》和《TCP/IP 详解》这两本厚厚的书,不过这次就不会那么吃力了。 48 | 49 | 后面回看这两本书时,我也没有选择从头看到尾,因为有些内容和在新手村看的书的内容重叠了,而且由于在新手村里知道了哪几个协议是常见的,于是就选择了这几个协议的章节进行深入学习,比如: 50 | 51 | - 我想进一步学习 TCP 协议的特性,于是就跳到《TCP/IP 详解》书里讲 TCP 协议的几个章节,我就从中学到了 TCP 流量控制、超时重传、拥塞控制等等。 52 | - 我想进一步学习 IP 协议,于是就跳到《计算机网络 - 自顶向下》书里讲 IP 协议的章节,我就从中学到了 IP 协议更多的细节,IP 包头的各个字段用途、寻址、路由转发的原理等等。 53 | 54 | 看了黑皮书,我也深刻感受到黑皮系列的书确实经典,知识体系很全面,也很细节。 55 | 56 | 但是这种大且全的书并不意味着适合入门,新手很容易就在各种细节中迷失,而且书上有些不常用的协议我们是可以选择不看的,如果不知道重点很容易就把时间浪费在这些地方,得不偿失。 57 | 58 | 我是在新手村学习里抓到学习计算机网络的方向,也就是把「**键入网址,到网页显示,期间发生了什么?**」这个问题所涉及到的协议都要掌握,比如 HTTP、DNS、TCP、UDP、IP、ARP、MAC 等等,然后再查黑皮书对应的章节来深入学习对应的协议。 59 | 60 | 不仅仅是计算机网络,我在学习操作系统、计算机组成原理、网络编程等等也是用这套方法,都是先看新手村的书,得知了哪些是重点知识后,再跳到黑皮书里对应该知识的章节进行深入学习。 61 | 62 | 当初在学网络编程的时候,看见网上的人都说 UNP(Unix 网络编程)、APUE(Unix 高级环境编程)这两本书是网络编程圣经的书,那么好学的小林,那肯定毫无犹豫买了。 63 | 64 | 书到货后,我瞬间就懵逼了,这两本书是我买过最厚的书,这尼玛怎么学? 65 | 66 | 跟着书本的节奏,学了一段的时间,是懂了些 Linux 网络和系统 API 的用法,摸索来摸索去都是各个 API 的细节,**始终不知道高并发网络框架是如何实现的**。 67 | 68 | ![图片](https://img-blog.csdnimg.cn/img_convert/3ec31f475de32791bb6bfaf32d86cf90.png) 69 | 70 | 后面我又重新找了一波关于网络编程的书,找到了这两本:《TCP/IP 网络编程》和《Linux 高性能服务器编程》。 71 | 72 | - 《TCP/IP 网络编程》绝对是新手村级别的书,书里的内容不会有过多的术语,作者都用大白话来表达,配图也很清晰,也有介绍我想知道的网络框架,虽然是比较基础的多进程服务端模型、多线程服务端模型、异步 IO 模型。而且最后一章实现了简单的 HTTP 服务端,让我知道了从代码角度是怎么解析 HTTP 报文的,以及状态机是如何实现和运转的。 73 | - 《Linux 高性能服务器编程》这本书主要是网络框架为主,前几章关于网络基础知识对于掌握了计算机网络知识的同学可以直接跳过的,你看,很多知识是想通的,当我们知道掌握了这块知识后,在学习新一本书的时候,就可以跳过重叠的内容。在这本书我学到了,Reactor、Proactor、信号、定时器、多进程编程、多线程编程、进程池和线程池等。 74 | 75 | 这两本书让我大概知道了如果一个服务端要服务多个客户端时,不是就简单写个 socket 编程就完事,而是还要结合 IO 多路复用 + 多线程的思想,也就是 Reactor 的设计理念,知道了这些事情后,后面我在看很多开源框架的网络模型时候,发现大多数基于 Reactor 的思想来实现的。 76 | 77 | 有了网络编程总体的视角后,在需要深入理解 socket api 中各种属性设置(超时、非阻塞 IO、阻塞 IO 等)和异常处理就要回归 APUE 这本书。 78 | 79 | 到这里我才知道 UNP 和 APUE 为什么会被称为网络编程圣经级别的书,原因是书里各种细节和异常都写的很全,也很细致,可以应对工作中很多问题。 80 | 81 | 但是事实证明,它并不是个入门级的书,所以 UNP 和 APUE 的用途比较像字典,在需要的时候去查阅就好。 82 | 83 | 学习算机组成也一样,我先看《程序是怎么样跑起来的》这本书,知道了程序跑起来的大概过程以及涉及到的知识点,然后带着这个问题,从《计算机组成与设计》这本黑皮书找到每一部分的细节,通过进一步学习,知道了程序编译过程,知道了 Intel x86 的指令结构,知道了计算机是如何存储并计算浮点数的,知道了 CPU 执行程序的工作流程,知道了计算机存储结构金字塔模型等等。 84 | 85 | 所以,大家在学习的时候,应该避免直接学大而全的书,我们要先从入门级别的书看起,抓住了主线重点知识后,再通过查阅这类大而全的书来进行深入学习。 86 | 87 | ------ 88 | 89 | ## 不要只局限学一本书 90 | 91 | 我在学习的时候,有个习惯,喜欢找同类型的书一起学,就不会说学操作系统的时候,就只看一本理论书,而是结合 Linux 系统编程和内核分析的书一起看,**一层层的深入一个知识点**。 92 | 93 | 比如,我在学习操作系统的时候,在《现代操作系统》学了「进程与线程」的内容,而这本书介绍的内容比较概念性的,知识点也比较笼统,不够具体。 94 | 95 | 然后我就会去学《Unix 高级环境编程》第 7 章「进程环境」、第 8 章「进程控制」、第 11 章「线程」、第 12 章「线程控制」、第 15 章「进程间通信」,这一系列章节看完后,就知道了 Linux 是如果通过创建进程和线程,不只局限于理论了,还学会了应用。 96 | 97 | 当然这还不够,我还会去学《深入 Linux 内核架构》第 2 章关于进程和线程的 Linux 源码分析,发现 Linux 中进程和线程实际上都是用一个结构体 `task_struct` 来表示的。让我很惊叹的是,Linux 操作系统对于进程和线程的创建,都是调用 `do_fork` 函数实现的。 98 | 99 | ![图片](https://img-blog.csdnimg.cn/img_convert/0e8f4dd1c1ad0759ddff6df29e8ff4e5.png) 100 | 101 | 只不过传递的参数不同,通过参数的不同来控制是复制父进程的资源(内存、文件描述、信号量等),还是引用父进程的资源,这样会更加深刻知道进程和线程的区别。 102 | 103 | 再比如,我在学习计算机网络的时候,在《图解 TCP/IP》学到了第六章关于 TCP 超时重传、流量控制、拥塞控制等内容,这本书讲的比较浅。 104 | 105 | 为了更深入理解 TCP,我就会去看《TCP/IP 详解》第 17 到 24 章,这几章都是详细介绍了 TCP,在这里会学到更全面的 TCP,比如 同时打开或关闭、negle 算法、往返时间 RTT 的计算、还有拥塞控制、快速重传、快速恢复、慢启动这些过程中的拥塞窗口是怎么变化的等等。 106 | 107 | 但是我在学《TCP/IP 详解》遇到了点困难,因为书里的案例有些地方看的不清晰,也不容易懂,特别是那些 TCP 抓包图,看到瞎眼。 108 | 109 | 后面我找到了本神书:《TCP/IP Guide》,很可惜只有英文的,我只看了这本书讲滑动窗口和流量控制的章节,因为这本书的精华就是这两个,其他的一般般,这两个章节的配图特别多,也很清晰。 110 | 111 | ![图片](https://img-blog.csdnimg.cn/img_convert/a3359404cfec81917734560ec285f215.png) 112 | 113 | 我就在这知道了发送窗口和接收窗口的工作过程,也知道了滑动窗口对流量的影响,也知道了操作系统内存紧张的时候,也会影响滑动窗口,以及糊涂窗口综合症等等。 114 | 115 | 所以在学习一个知识的时候,大家不一定要把一本书从头看到尾后,才去学另外一本书。 116 | 117 | 最好的方式是在一本书看完某个章节的知识点后,马上去学另外一本相对比较深入的书的对应章节的内容,这样一层一层的深入下去,你对这个知识点的掌握就会很深刻了。 118 | 119 | ------ 120 | 121 | ## 不要只看不动手 122 | 123 | 计算机类的知识都比较庞大,单纯只看很容易就忘记的了,当然即使做了笔记也会忘记。 124 | 125 | 就像小林写了很多文章,每篇文章的内容我也不一定都记得住,但是当我回看文章后,知识点很快会被唤醒起来。 126 | 127 | 所以记笔记的好处在于后面复习的时候,可以很快就能回想起来。 128 | 129 | 记笔记的方式有很多种,手写在笔记本、在书上标注、在 world 文档记录等等,但这些我觉得都不是好的方式。 130 | 131 | 我觉得比较好的方式是**思维导图**,把思维导图当作一课自己的知识树,每深入学一个知识点的时候,就开一个分支去记录,记录的内容最好是用自己的话来描述,而不是复制书上的内容,这样只是单纯的 copy,最好经过自己大脑的思考,用自己的话做个小总结,这样的知识点不容易忘。 132 | 133 | 还有很多知识其实可以结合**生活中的场景**来记忆,这样想忘记都难,比如阻塞 IO、非阻塞 IO、同步 IO 和异步 IO,我之前文章用去饭堂打菜的场景来介绍它们之间的区别。 134 | 135 | ![图片](https://img-blog.csdnimg.cn/img_convert/a8db34dc8f3ed25b0910579b605450a7.png) 136 | 137 | 再比如介绍各种进程调度算法,我之前文章用银行业务办理的场景来介绍。 138 | 139 | ![图片](https://img-blog.csdnimg.cn/img_convert/6e230e3982fe3f8a4a6cf19e1ca1c100.png) 140 | 141 | ------ 142 | 143 | ## 总结 144 | 145 | 最后做个总结,回答开头的问题。 146 | 147 | > 书看不懂,容易放弃,怎么办? 148 | 149 | 不要一开始选择困难模式,也就是不要一开始选择大而全的书,这类书一般不适合入门学习。 150 | 151 | 我们先要找新手村级别的书来入门,新手村的书一般很快就看完的,看完后我们大概就知道这类书籍的重点知识,然后再通过查阅这些大而全的书的目录来学习对应章节的内容。 152 | 153 | > 看书的效率很低,怎么办? 154 | 155 | 其实书并不一定要全部从头看完的,而且也不要固执到一直只看一本书。 156 | 157 | 最好在学习某个知识点的时候,通过看多本书来一层层的学习这个知识点,这样你学起来的知识点会比较全面,也更加深入。 158 | 159 | 按这种方式学,你会发现很多书都被你不经意间看了 7788 的。 160 | 161 | > 做了很多笔记,依然过会就忘记,怎么办? 162 | 163 | 做笔记建议使用思维导图,把思维导图当作一课自己的知识树,每深入学一个知识点的时候,就开一个分支去记录。 164 | 165 | 在记录笔记的时候,尽量少 copy 书上的内容,最好还是经过自己思考后用自己的话输出的笔记,而且可以搭配生活场景来加深记忆点。 -------------------------------------------------------------------------------- /mysql/README.md: -------------------------------------------------------------------------------- 1 | # 图解 MySQL 介绍 2 | 3 | 《图解 MySQL》目前还在连载更新中,大家不要催啦:joy: ,更新完会第一时间整理 PDF 的。 4 | 5 | 目前已经更新好的文章: 6 | 7 | - **基础篇**:point_down: 8 | 9 | - [执行一条 SQL 查询语句,期间发生了什么?](/mysql/base/how_select.md) 10 | - [MySQL 一行记录是怎么存储的?](/mysql/base/row_format.md) 11 | 12 | - **索引篇** :point_down: 13 | 14 | - [索引常见面试题](/mysql/index/index_interview.md) 15 | - [从数据页的角度看 B+ 树](/mysql/index/page.md) 16 | - [为什么 MySQL 采用 B+ 树作为索引?](/mysql/index/why_index_chose_bpuls_tree.md) 17 | - [MySQL 单表不要超过 2000W 行,靠谱吗?](/mysql/index/2000w.md) 18 | - [索引失效有哪些?](/mysql/index/index_lose.md) 19 | - [MySQL 使用 like“%x“,索引一定会失效吗?](/mysql/index/index_issue.md) 20 | - [count(\*) 和 count(1) 有什么区别?哪个性能最好?](/mysql/index/count.md) 21 | 22 | - **事务篇** :point_down: 23 | - [事务隔离级别是怎么实现的?](/mysql/transaction/mvcc.md) 24 | - [MySQL 可重复读隔离级别,完全解决幻读了吗?](/mysql/transaction/phantom.md) 25 | 26 | - **锁篇** :point_down: 27 | - [MySQL 有哪些锁?](/mysql/lock/mysql_lock.md) 28 | - [MySQL 是怎么加锁的?](/mysql/lock/how_to_lock.md) 29 | - [update 没加索引会锁全表?](/mysql/lock/update_index.md) 30 | - [MySQL 记录锁 + 间隙锁可以防止删除操作而导致的幻读吗?](/mysql/lock/lock_phantom.md) 31 | - [MySQL 死锁了,怎么办?](/mysql/lock/deadlock.md) 32 | - [字节面试:加了什么锁,导致死锁的?](/mysql/lock/show_lock.md) 33 | 34 | - **日志篇** :point_down: 35 | 36 | - [undo log、redo log、binlog 有什么用?](/mysql/log/how_update.md) 37 | 38 | - **内存篇** :point_down: 39 | 40 | - [揭开 Buffer_Pool 的面纱](/mysql/buffer_pool/buffer_pool.md) 41 | 42 | ---- 43 | 44 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 45 | 46 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /mysql/buffer_pool/README.md: -------------------------------------------------------------------------------- 1 | buffer poll、Change Buffer、Adaptive Hash Index、Change Buffer、Doublewrite Buffer 正在赶稿的路上。。。。。 -------------------------------------------------------------------------------- /mysql/index/count.md: -------------------------------------------------------------------------------- 1 | # count(*) 和 count(1) 有什么区别?哪个性能最好? 2 | 3 | 大家好,我是小林。 4 | 5 | 当我们对一张数据表中的记录进行统计的时候,习惯都会使用 count 函数来统计,但是 count 函数传入的参数有很多种,比如 count(1)、count(`*`)、count(字段) 等。 6 | 7 | 到底哪种效率是最好的呢?是不是 count(`*`) 效率最差? 8 | 9 | 我曾经以为 count(`*`) 是效率最差的,因为认知上 `selete * from t` 会读取所有表中的字段,所以凡事带有 `*` 字符的就觉得会读取表中所有的字段,当时网上有很多博客也这么说。 10 | 11 | 但是,当我深入 count 函数的原理后,被啪啪啪的打脸了! 12 | 13 | 不多说,发车! 14 | 15 | ![图片](https://img-blog.csdnimg.cn/img_convert/d9b9817e92f805e9a16faf31a2c10d9a.png) 16 | 17 | ## 哪种 count 性能最好? 18 | 19 | 我先直接说结论: 20 | 21 | ![图片](https://img-blog.csdnimg.cn/img_convert/af711033aa3423330d3a4bc6baeb9532.png) 22 | 23 | 要弄明白这个,我们得要深入 count 的原理,以下内容基于常用的 innodb 存储引擎来说明。 24 | 25 | ### count() 是什么? 26 | 27 | count() 是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意表达式,该函数作用是**统计符合查询条件的记录中,函数指定的参数不为 NULL 的记录有多少个**。 28 | 29 | 假设 count() 函数的参数是字段名,如下: 30 | 31 | ```sql 32 | select count(name) from t_order; 33 | ``` 34 | 35 | 这条语句是统计「t_order 表中,name 字段不为 NULL 的记录」有多少个。也就是说,如果某一条记录中的 name 字段的值为 NULL,则就不会被统计进去。 36 | 37 | 再来假设 count() 函数的参数是数字 1 这个表达式,如下: 38 | 39 | ```sql 40 | select count(1) from t_order; 41 | ``` 42 | 43 | 这条语句是统计「t_order 表中,1 这个表达式不为 NULL 的记录」有多少个。 44 | 45 | 1 这个表达式就是单纯数字,它永远都不是 NULL,所以上面这条语句,其实是在统计 t_order 表中有多少个记录。 46 | 47 | ### count(主键字段) 执行过程是怎样的? 48 | 49 | 在通过 count 函数统计有多少个记录时,MySQL 的 server 层会维护一个名叫 count 的变量。 50 | 51 | server 层会循环向 InnoDB 读取一条记录,如果 count 函数指定的参数不为 NULL,那么就会将变量 count 加 1,直到符合查询的全部记录被读完,就退出循环。最后将 count 变量的值发送给客户端。 52 | 53 | InnoDB 是通过 B+ 树来保存记录的,根据索引的类型又分为聚簇索引和二级索引,它们区别在于,聚簇索引的叶子节点存放的是实际数据,而二级索引的叶子节点存放的是主键值,而不是实际数据。 54 | 55 | 用下面这条语句作为例子: 56 | 57 | ```sql 58 | //id 为主键值 59 | select count(id) from t_order; 60 | ``` 61 | 62 | 如果表里只有主键索引,没有二级索引时,那么,InnoDB 循环遍历聚簇索引,将读取到的记录返回给 server 层,然后读取记录中的 id 值,就会 id 值判断是否为 NULL,如果不为 NULL,就将 count 变量加 1。 63 | 64 | ![图片](https://img-blog.csdnimg.cn/img_convert/9bb4f32ac843467684a2664d4db61ae3.png) 65 | 66 | 但是,如果表里有二级索引时,InnoDB 循环遍历的对象就不是聚簇索引,而是二级索引。 67 | 68 | ![图片](https://img-blog.csdnimg.cn/img_convert/aac550602ef1022e0b45020dbe0f716a.png) 69 | 70 | 这是因为相同数量的二级索引记录可以比聚簇索引记录占用更少的存储空间,所以二级索引树比聚簇索引树小,这样遍历二级索引的 I/O 成本比遍历聚簇索引的 I/O 成本小,因此「优化器」优先选择的是二级索引。 71 | 72 | ### count(1) 执行过程是怎样的? 73 | 74 | 用下面这条语句作为例子: 75 | 76 | ```plain 77 | select count(1) from t_order; 78 | ``` 79 | 80 | 如果表里只有主键索引,没有二级索引时。 81 | 82 | ![图片](https://img-blog.csdnimg.cn/img_convert/e630fdc5897b5c5dbc332e8838afa1fc.png) 83 | 84 | 那么,InnoDB 循环遍历聚簇索引(主键索引),将读取到的记录返回给 server 层,**但是不会读取记录中的任何字段的值**,因为 count 函数的参数是 1,不是字段,所以不需要读取记录中的字段值。参数 1 很明显并不是 NULL,因此 server 层每从 InnoDB 读取到一条记录,就将 count 变量加 1。 85 | 86 | 可以看到,count(1) 相比 count(主键字段) 少一个步骤,就是不需要读取记录中的字段值,所以通常会说 count(1) 执行效率会比 count(主键字段) 高一点。 87 | 88 | 但是,如果表里有二级索引时,InnoDB 循环遍历的对象就二级索引了。 89 | 90 | ![图片](https://img-blog.csdnimg.cn/img_convert/01e83441a7721f0864deb1ac14ad8ea6.png) 91 | 92 | ### count(*) 执行过程是怎样的? 93 | 94 | 看到 `*` 这个字符的时候,是不是大家觉得是读取记录中的所有字段值? 95 | 96 | 对于 `select *` 这条语句来说是这个意思,但是在 count(*) 中并不是这个意思。 97 | 98 | **count(`*`) 其实等于 count(`0`)**,也就是说,当你使用 count(`*`) 时,MySQL 会将 `*` 参数转化为参数 0 来处理。 99 | 100 | ![图片](https://img-blog.csdnimg.cn/img_convert/27b229f049b27898f3a86c7da7e26114.png) 101 | 102 | 所以,**count(\*) 执行过程跟 count(1) 执行过程基本一样的**,性能没有什么差异。 103 | 104 | 在 MySQL 5.7 的官方手册中有这么一句话: 105 | 106 | *InnoDB handles SELECT COUNT(`*`) and SELECT COUNT(`1`) operations in the same way. There is no performance difference.* 107 | 108 | *翻译:InnoDB 以相同的方式处理 SELECT COUNT(`*`)和 SELECT COUNT(`1`)操作,没有性能差异。* 109 | 110 | 而且 MySQL 会对 count(*) 和 count(1) 有个优化,如果有多个二级索引的时候,优化器会使用 key_len 最小的二级索引进行扫描。 111 | 112 | 只有当没有二级索引的时候,才会采用主键索引来进行统计。 113 | 114 | ### count(字段) 执行过程是怎样的? 115 | 116 | count(字段) 的执行效率相比前面的 count(1)、count(*)、count(主键字段) 执行效率是最差的。 117 | 118 | 用下面这条语句作为例子: 119 | 120 | ```sql 121 | // name不是索引,普通字段 122 | select count(name) from t_order; 123 | ``` 124 | 125 | 对于这个查询来说,会采用全表扫描的方式来计数,所以它的执行效率是比较差的。 126 | 127 | ![图片](https://img-blog.csdnimg.cn/img_convert/f24dfeb85e2cfce0e4dc3a17b893b3f5.png) 128 | 129 | ### 小结 130 | 131 | count(1)、 count(*)、count(主键字段) 在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。 132 | 133 | 所以,如果要执行 count(1)、count(*)、count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。 134 | 135 | 再来,就是不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。 136 | 137 | ## 为什么要通过遍历的方式来计数? 138 | 139 | 你可能会好奇,为什么 count 函数需要通过遍历的方式来统计记录个数? 140 | 141 | 我前面将的案例都是基于 Innodb 存储引擎来说明的,但是在 MyISAM 存储引擎里,执行 count 函数的方式是不一样的,通常在没有任何查询条件下的 count(*),MyISAM 的查询速度要明显快于 InnoDB。 142 | 143 | 使用 MyISAM 引擎时,执行 count 函数只需要 O(1 ) 复杂度,这是因为每张 MyISAM 的数据表都有一个 meta 信息有存储了 row_count 值,由表级锁保证一致性,所以直接读取 row_count 值就是 count 函数的执行结果。 144 | 145 | 而 InnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的,所以无法像 MyISAM 一样,只维护一个 row_count 变量。 146 | 147 | 举个例子,假设表 t_order 有 100 条记录,现在有两个会话并行以下语句: 148 | 149 | ![图片](https://img-blog.csdnimg.cn/img_convert/04d714293f5c687810562e984b67d2e7.png) 150 | 151 | 在会话 A 和会话 B 的最后一个时刻,同时查表 t_order 的记录总个数,可以发现,显示的结果是不一样的。所以,在使用 InnoDB 存储引擎时,就需要扫描表来统计具体的记录。 152 | 153 | 而当带上 where 条件语句之后,MyISAM 跟 InnoDB 就没有区别了,它们都需要扫描表来进行记录个数的统计。 154 | 155 | ## 如何优化 count(*)? 156 | 157 | 如果对一张大表经常用 count(*) 来做统计,其实是很不好的。 158 | 159 | 比如下面我这个案例,表 t_order 共有 1200+ 万条记录,我也创建了二级索引,但是执行一次 `select count(*) from t_order` 要花费差不多 5 秒! 160 | 161 | ![图片](https://img-blog.csdnimg.cn/img_convert/74a4359b58dc6ed41a241e425f43764d.png) 162 | 163 | 面对大表的记录统计,我们有没有什么其他更好的办法呢? 164 | 165 | ### 第一种,近似值 166 | 167 | 如果你的业务对于统计个数不需要很精确,比如搜索引擎在搜索关键词的时候,给出的搜索结果条数是一个大概值。 168 | 169 | ![图片](https://img-blog.csdnimg.cn/img_convert/cd18879de0c0b37660f53a5f1af3d172.png) 170 | 171 | 这时,我们就可以使用 show table status 或者 explain 命令来表进行估算。 172 | 173 | 执行 explain 命令效率是很高的,因为它并不会真正的去查询,下图中的 rows 字段值就是 explain 命令对表 t_order 记录的估算值。 174 | 175 | ![图片](https://img-blog.csdnimg.cn/img_convert/7590623443e8f225e5652109e6d9e3d2.png) 176 | 177 | ### 第二种,额外表保存计数值 178 | 179 | 如果是想精确的获取表的记录总数,我们可以将这个计数值保存到单独的一张计数表中。 180 | 181 | 当我们在数据表插入一条记录的同时,将计数表中的计数字段 + 1。也就是说,在新增和删除操作时,我们需要额外维护这个计数表。 182 | 183 | ---- 184 | 185 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 186 | 187 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /mysql/index/index_issue.md: -------------------------------------------------------------------------------- 1 | # MySQL 使用 like“%x“,索引一定会失效吗? 2 | 3 | 4 | 5 | 大家好,我是小林。 6 | 7 | 昨天发了一篇关于索引失效的文章:[谁还没碰过索引失效呢](http://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247503394&idx=1&sn=6e5b7b2c9bd9002a4b2dfa69273069b3&chksm=f98d8a88cefa039e726f1196ba14210ddbe49b5fcbb6da620778a7497fa25404433ef0b76268&scene=21#wechat_redirect) 8 | 9 | 我在文末留了一个有点意思的思考题: 10 | 11 | ![图片](https://img-blog.csdnimg.cn/img_convert/c3e14ca7c5581a84820f7a9d647d4d14.png) 12 | 13 | 14 | 15 | 这个思考题其实是出自于,我之前这篇文章「[一条 SQL 语句引发的思考](http://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247495686&idx=2&sn=dfa18870d8cd2f430f893d402b9f4e54&chksm=f98db4accefa3dba680c1b343700ef87d184c45d4d7739bb0263cece3c1b21d0ca5f875736f6&scene=21#wechat_redirect)」中留言区一位读者朋友出的问题。 16 | 17 | 很多读者都在留言区说了自己的想法,也有不少读者私聊我答案到底是什么? 18 | 19 | 所以,我今晚就跟大家聊聊这个思考题。 20 | 21 | ### 题目一 22 | 23 | 题目一很简单,相信大家都能分析出答案,我昨天分享的索引失效文章里也提及过。 24 | 25 | 题目 1 的数据库表如下,id 是主键索引,name 是二级索引,其他字段都是非索引字段。 26 | 27 | ![图片](https://img-blog.csdnimg.cn/img_convert/f46694a7f2c91443b616eadf8526c09a.png) 28 | 29 | 这四条模糊匹配的查询语句,第一条和第二条都会走索引扫描,而且都是选择扫描二级索引(index_name),我贴个第二条查询语句的执行计划结果图: 30 | 31 | ![图片](https://img-blog.csdnimg.cn/img_convert/febffda129751df080f734c1fc7980f1.png) 32 | 33 | 34 | 35 | 而第三和第四条会发生索引失效,执行计划的结果 type= ALL,代表了全表扫描。 36 | 37 | ![图片](https://img-blog.csdnimg.cn/img_convert/52952f616b03318e196b6e1207b888ad.png) 38 | 39 | 40 | 41 | ### 题目二 42 | 43 | 题目 2 的数据库表特别之处在于,只有两个字段,一个是主键索引 id,另外一个是二级索引 name。 44 | 45 | ![图片](https://img-blog.csdnimg.cn/img_convert/a80a15eb8cd65eec777908282e04be2a.png) 46 | 47 | 针对题目 2 的数据表,第一条和第二条模糊查询语句也是一样可以走索引扫描,第二条查询语句的执行计划如下,Extra 里的 Using index 说明用上了覆盖索引: 48 | 49 | ![图片](https://img-blog.csdnimg.cn/img_convert/d250a6ba3068ef41da9039974dad206a.png) 50 | 51 | 我们来看一下第三条查询语句的执行计划(第四条也是一样的结果): 52 | 53 | ![图片](https://img-blog.csdnimg.cn/img_convert/948ac3e63c36a93101860e7da11ddc42.png) 54 | 55 | 从执行计划的结果中,可以看到 key=index_name,也就是说用上了二级索引,而且从 Extra 里的 Using index 说明用上了覆盖索引。 56 | 57 | 这是为什么呢? 58 | 59 | 首先,这张表的字段没有「非索引」字段,所以 `select *` 相当于 `select id,name`,然后**这个查询的数据都在二级索引的 B+ 树,因为二级索引的 B+ 树的叶子节点包含「索引值 + 主键值」,所以查二级索引的 B+ 树就能查到全部结果了,这个就是覆盖索引。** 60 | 61 | 但是执行计划里的 type 是 `index`,这代表着是通过全扫描二级索引的 B+ 树的方式查询到数据的,也就是遍历了整颗索引树。 62 | 63 | 而第一和第二条查询语句的执行计划中 type 是 `range`,表示对索引列进行范围查询,也就是利用了索引树的有序性的特点,通过查询比较的方式,快速定位到了数据行。 64 | 65 | 所以,type=range 的查询效率会比 type=index 的高一些。 66 | 67 | > 为什么选择全扫描二级索引树,而不扫描聚簇索引树呢? 68 | 69 | 因为二级索引树的记录东西很少,就只有「索引列 + 主键值」,而聚簇索引记录的东西会更多,比如聚簇索引中的叶子节点则记录了主键值、事务 id、用于事务和 MVCC 的回滚指针以及所有的剩余列。 70 | 71 | 再加上,这个 select * 不用执行回表操作。 72 | 73 | 所以,MySQL 优化器认为直接遍历二级索引树要比遍历聚簇索引树的成本要小的多,因此 MySQL 选择了「全扫描二级索引树」的方式查询数据。 74 | 75 | > 为什么这个数据表加了非索引字段,执行同样的查询语句后,怎么变成走的是全表扫描呢? 76 | 77 | 加了其他字段后,`select * from t_user where name like "%xx";` 要查询的数据就不能只在二级索引树里找了,得需要回表操作才能完成查询的工作,再加上是左模糊匹配,无法利用索引树的有序性来快速定位数据,所以得在二级索引树逐一遍历,获取主键值后,再到聚簇索引树检索到对应的数据行,这样实在太累了。 78 | 79 | 所以,优化器认为上面这样的查询过程的成本实在太高了,所以直接选择全表扫描的方式来查询数据。 80 | 81 | ------ 82 | 83 | 从这个思考题我们知道了,使用左模糊匹配(like "%xx")并不一定会走全表扫描,关键还是看数据表中的字段。 84 | 85 | 如果数据库表中的字段只有主键 + 二级索引,那么即使使用了左模糊匹配,也不会走全表扫描(type=all),而是走全扫描二级索引树 (type=index)。 86 | 87 | 再说一个相似,我们都知道联合索引要遵循最左匹配才能走索引,但是如果数据库表中的字段都是索引的话,即使查询过程中,没有遵循最左匹配原则,也是走全扫描二级索引树 (type=index),比如下图: 88 | 89 | ![图片](https://img-blog.csdnimg.cn/img_convert/35d04bff09bb638727245c7f9aa95b5c.png) 90 | 91 | 就说到这了,下次见啦 92 | 93 | ---- 94 | 95 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 96 | 97 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 98 | -------------------------------------------------------------------------------- /mysql/index/page.md: -------------------------------------------------------------------------------- 1 | # 从数据页的角度看 B+ 树 2 | 3 | 大家好,我是小林。 4 | 5 | 大家背八股文的时候,都知道 MySQL 里 InnoDB 存储引擎是采用 B+ 树来组织数据的。 6 | 7 | 这点没错,但是大家知道 B+ 树里的节点里存放的是什么呢?查询数据的过程又是怎样的? 8 | 9 | 这次,我们**从数据页的角度看 B+ 树**,看看每个节点长啥样。 10 | 11 | ![图片](https://img-blog.csdnimg.cn/img_convert/f7696506a3c1c94621fcbad10341f1a8.png) 12 | 13 | ## InnoDB 是如何存储数据的? 14 | 15 | MySQL 支持多种存储引擎,不同的存储引擎,存储数据的方式也是不同的,我们最常使用的是 InnoDB 存储引擎,所以就跟大家图解下 InnoDB 是如何存储数据的。 16 | 17 | 记录是按照行来存储的,但是数据库的读取并不以「行」为单位,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。 18 | 19 | 因此,**InnoDB 的数据是按「数据页」为单位来读写的**,也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。 20 | 21 | 数据库的 I/O 操作的最小单位是页,**InnoDB 数据页的默认大小是 16KB**,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。 22 | 23 | 数据页包括七个部分,结构如下图: 24 | 25 | ![图片](https://img-blog.csdnimg.cn/img_convert/243b1466779a9e107ae3ef0155604a17.png) 26 | 27 | 这 7 个部分的作用如下图: 28 | 29 | ![图片](https://img-blog.csdnimg.cn/img_convert/fabd6dadd61a0aa342d7107213955a72.png) 30 | 31 | 在 File Header 中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表,如下图所示: 32 | 33 | ![图片](https://img-blog.csdnimg.cn/img_convert/557d17e05ce90f18591c2305871af665.png) 34 | 35 | 采用链表的结构是让数据页之间不需要是物理上的连续的,而是逻辑上的连续。 36 | 37 | 数据页的主要作用是存储记录,也就是数据库的数据,所以重点说一下数据页中的 User Records 是怎么组织数据的。 38 | 39 | **数据页中的记录按照「主键」顺序组成单向链表**,单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。 40 | 41 | 因此,数据页中有一个**页目录**,起到记录的索引作用,就像我们书那样,针对书中内容的每个章节设立了一个目录,想看某个章节的时候,可以查看目录,快速找到对应的章节的页数,而数据页中的页目录就是为了能快速找到记录。 42 | 43 | 那 InnoDB 是如何给记录创建页目录的呢?页目录与记录的关系如下图: 44 | 45 | ![图片](https://img-blog.csdnimg.cn/img_convert/261011d237bec993821aa198b97ae8ce.png) 46 | 47 | 页目录创建的过程如下: 48 | 49 | 1. 将所有的记录划分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录; 50 | 2. 每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段(上图中粉红色字段) 51 | 3. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),**每个槽相当于指针指向了不同组的最后一个记录**。 52 | 53 | 从图可以看到,**页目录就是由多个槽组成的,槽相当于分组记录的索引**。然后,因为记录是按照「主键值」从小到大排序的,所以**我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录**,无需从最小记录开始遍历整个页中的记录链表。 54 | 55 | 以上面那张图举个例子,5 个槽的编号分别为 0,1,2,3,4,我想查找主键为 11 的用户记录: 56 | 57 | - 先二分得出槽中间位是 (0+4)/2=2,2 号槽里最大的记录为 8。因为 11 > 8,所以需要从 2 号槽后继续搜索记录; 58 | - 再使用二分搜索出 2 号和 4 号槽的中间位是 (2+4)/2= 3,3 号槽里最大的记录为 12。因为 11 < 12,所以主键为 11 的记录在 3 号槽里; 59 | - 这里有个问题,**「槽对应的值都是这个组的主键最大的记录,如何找到组里最小的记录」**?比如槽 3 对应最大主键是 12 的记录,那如何找到最小记录 9。解决办法是:通过槽 3 找到 槽 2 对应的记录,也就是主键为 8 的记录。主键为 8 的记录的下一条记录就是槽 3 当中主键最小的 9 记录,然后开始向下搜索 2 次,定位到主键为 11 的记录,取出该条记录的信息即为我们想要查找的内容。 60 | 61 | 看到第三步的时候,可能有的同学会疑问,如果某个槽内的记录很多,然后因为记录都是单向链表串起来的,那这样在槽内查找某个记录的时间复杂度不就是 O(n) 了吗? 62 | 63 | 这点不用担心,InnoDB 对每个分组中的记录条数都是有规定的,槽内的记录就只有几条: 64 | 65 | - 第一个分组中的记录只能有 1 条记录; 66 | - 最后一个分组中的记录条数范围只能在 1-8 条之间; 67 | - 剩下的分组中记录条数范围只能在 4-8 条之间。 68 | 69 | ## B+ 树是如何进行查询的? 70 | 71 | 上面我们都是在说一个数据页中的记录检索,因为一个数据页中的记录是有限的,且主键值是有序的,所以通过对所有记录进行分组,然后将组号(槽号)存储到页目录,使其起到索引作用,通过二分查找的方法快速检索到记录在哪个分组,来降低检索的时间复杂度。 72 | 73 | 但是,当我们需要存储大量的记录时,就需要多个数据页,这时我们就需要考虑如何建立合适的索引,才能方便定位记录所在的页。 74 | 75 | 为了解决这个问题,**InnoDB 采用了 B+ 树作为索引**。磁盘的 I/O 操作次数对索引的使用效率至关重要,因此在构造索引的时候,我们更倾向于采用“矮胖”的 B+ 树数据结构,这样所需要进行的磁盘 I/O 次数更少,而且 B+ 树 更适合进行关键字的范围查询。 76 | 77 | InnoDB 里的 B+ 树中的**每个节点都是一个数据页**,结构示意图如下: 78 | 79 | ![图片](https://img-blog.csdnimg.cn/img_convert/7c635d682bd3cdc421bb9eea33a5a413.png) 80 | 81 | 通过上图,我们看出 B+ 树的特点: 82 | 83 | - 只有叶子节点(最底层的节点)才存放了数据,非叶子节点(其他上层节)仅用来存放目录项作为索引。 84 | - 非叶子节点分为不同层次,通过分层来降低每一层的搜索量; 85 | - 所有节点按照索引键大小排序,构成一个双向链表,便于范围查询; 86 | 87 | 我们再看看 B+ 树如何实现快速查找主键为 6 的记录,以上图为例子: 88 | 89 | - 从根节点开始,通过二分法快速定位到符合页内范围包含查询值的页,因为查询的主键值为 6,在[1, 7) 范围之间,所以到页 30 中查找更详细的目录项; 90 | - 在非叶子节点(页 30)中,继续通过二分法快速定位到符合页内范围包含查询值的页,主键值大于 5,所以就到叶子节点(页 16)查找记录; 91 | - 接着,在叶子节点(页 16)中,通过槽查找记录时,使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到主键为 6 的记录。 92 | 93 | 可以看到,在定位记录所在哪一个页时,也是通过二分法快速定位到包含该记录的页。定位到该页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。 94 | 95 | ## 聚簇索引和二级索引 96 | 97 | 另外,索引又可以分成聚簇索引和非聚簇索引(二级索引),它们区别就在于叶子节点存放的是什么数据: 98 | 99 | - 聚簇索引的叶子节点存放的是实际数据,所有完整的用户记录都存放在聚簇索引的叶子节点; 100 | - 二级索引的叶子节点存放的是主键值,而不是实际数据。 101 | 102 | 因为表的数据都是存放在聚簇索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚簇索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个。 103 | 104 | InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引: 105 | 106 | - 如果有主键,默认会使用主键作为聚簇索引的索引键; 107 | - 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键; 108 | - 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键; 109 | 110 | 一张表只能有一个聚簇索引,那为了实现非主键字段的快速搜索,就引出了二级索引(非聚簇索引/辅助索引),它也是利用了 B+ 树的数据结构,但是二级索引的叶子节点存放的是主键值,不是实际数据。 111 | 112 | 二级索引的 B+ 树如下图,数据部分为主键值: 113 | 114 | ![图片](https://img-blog.csdnimg.cn/img_convert/3104c8c3adf36e8931862fe8a0520f5d.png) 115 | 116 | 因此,**如果某个查询语句使用了二级索引,但是查询的数据不是主键值,这时在二级索引找到主键值后,需要去聚簇索引中获得数据行,这个过程就叫作「回表」,也就是说要查两个 B+ 树才能查到数据。不过,当查询的数据是主键值时,因为只在二级索引就能查询到,不用再去聚簇索引查,这个过程就叫作「索引覆盖」,也就是只需要查一个 B+ 树就能找到数据。** 117 | 118 | ## 总结 119 | 120 | InnoDB 的数据是按「数据页」为单位来读写的,默认数据页大小为 16 KB。每个数据页之间通过双向链表的形式组织起来,物理上不连续,但是逻辑上连续。 121 | 122 | 数据页内包含用户记录,每个记录之间用单向链表的方式组织起来,为了加快在数据页内高效查询记录,设计了一个页目录,页目录存储各个槽(分组),且主键值是有序的,于是可以通过二分查找法的方式进行检索从而提高效率。 123 | 124 | 为了高效查询记录所在的数据页,InnoDB 采用 b+ 树作为索引,每个节点都是一个数据页。 125 | 126 | 如果叶子节点存储的是实际数据的就是聚簇索引,一个表只能有一个聚簇索引;如果叶子节点存储的不是实际数据,而是主键值则就是二级索引,一个表中可以有多个二级索引。 127 | 128 | 在使用二级索引进行查找数据时,如果查询的数据能在二级索引找到,那么就是「索引覆盖」操作,如果查询的数据不在二级索引里,就需要先在二级索引找到主键值,需要去聚簇索引中获得数据行,这个过程就叫作「回表」。 129 | 130 | 关于索引的内容还有很多,比如索引失效、索引优化等等,这些内容我下次在讲啦! 131 | 132 | ---- 133 | 134 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 135 | 136 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 137 | -------------------------------------------------------------------------------- /mysql/lock/lock_phantom.md: -------------------------------------------------------------------------------- 1 | # MySQL 记录锁 + 间隙锁可以防止删除操作而导致的幻读吗? 2 | 3 | 大家好,我是小林。 4 | 5 | 昨天有位读者在美团二面的时候,被问到关于幻读的问题: 6 | 7 | ![](https://img-blog.csdnimg.cn/4c48fe8a02374754b1cf92591ae8d3b4.png) 8 | 9 | 面试官反问的大概意思是,**MySQL 记录锁 + 间隙锁可以防止删除操作而导致的幻读吗?** 10 | 11 | 答案是可以的。 12 | 13 | 接下来,通过几个小实验来证明这个结论吧,顺便再帮大家复习一下记录锁 + 间隙锁。 14 | 15 | ## 什么是幻读? 16 | 17 | 首先来看看 MySQL 文档是怎么定义幻读(Phantom Read)的: 18 | 19 | ***The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.*** 20 | 21 | 翻译:当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行。 22 | 23 | 举个例子,假设一个事务在 T1 时刻和 T2 时刻分别执行了下面查询语句,途中没有执行其他任何语句: 24 | 25 | ```sql 26 | SELECT * FROM t_test WHERE id > 100; 27 | ``` 28 | 29 | 只要 T1 和 T2 时刻执行产生的结果集是不相同的,那就发生了幻读的问题,比如: 30 | - T1 时间执行的结果是有 5 条行记录,而 T2 时间执行的结果是有 6 条行记录,那就发生了幻读的问题。 31 | - T1 时间执行的结果是有 5 条行记录,而 T2 时间执行的结果是有 4 条行记录,也是发生了幻读的问题。 32 | 33 | 34 | > MySQL 是怎么解决幻读的? 35 | 36 | MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了,详见这篇[文章](https://xiaolincoding.com/mysql/transaction/phantom.html)),解决的方案有两种: 37 | - 针对**快照读**(普通 select 语句),是**通过 MVCC 方式解决了幻读**,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。 38 | - 针对**当前读**(select ... for update 等语句),是**通过 next-key lock(记录锁 + 间隙锁)方式解决了幻读**,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。 39 | 40 | ## 实验验证 41 | 42 | 接下来,来验证「MySQL 记录锁 + 间隙锁**可以防止**删除操作而导致的幻读问题」的结论。 43 | 44 | 实验环境:MySQL 8.0 版本,可重复读隔离级。 45 | 46 | 现在有一张用户表(t_user),表里**只有一个主键索引**,表里有以下行数据: 47 | 48 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/75c5c503d7df4ad091bfc35708dce6c4.png) 49 | 50 | 现在有一个 A 事务执行了一条查询语句,查询到年龄大于 20 岁的用户共有 6 条行记录。 51 | 52 | ![](https://img-blog.csdnimg.cn/68dd89fc95aa42cf9b0c4251d4e9226c.png) 53 | 54 | 55 | 然后,B 事务执行了一条删除 id = 2 的语句: 56 | 57 | ![](https://img-blog.csdnimg.cn/2332fad58bc548ec917ba7ea44d09d30.png) 58 | 59 | 此时,B 事务的删除语句就陷入了**等待状态**,说明是无法进行删除的。 60 | 61 | 因此,MySQL 记录锁 + 间隙锁**可以防止**删除操作而导致的幻读问题。 62 | 63 | ### 加锁分析 64 | 65 | 问题来了,A 事务在执行 select ... for update 语句时,具体加了什么锁呢? 66 | 67 | 我们可以通过 `select * from performance_schema.data_locks\G;` 这条语句,查看事务执行 SQL 过程中加了什么锁。 68 | 69 | 输出的内容很多,共有 11 行信息,我删减了一些不重要的信息: 70 | 71 | ![请添加图片描述](https://img-blog.csdnimg.cn/90e68bf52b2c4e8a9127cfcbb0f0a322.png) 72 | 73 | 74 | 从上面输出的信息可以看到,共加了两种不同粒度的锁,分别是: 75 | 76 | - 表锁(`LOCK_TYPE: TABLE`):X 类型的意向锁; 77 | - 行锁(`LOCK_TYPE: RECORD`):X 类型的 next-key 锁; 78 | 79 | 这里我们重点关注「行锁」,图中 `LOCK_TYPE` 中的 `RECORD` 表示行级锁,而不是记录锁的意思: 80 | - 如果 LOCK_MODE 为 `X`,说明是 next-key 锁; 81 | - 如果 LOCK_MODE 为 `X, REC_NOT_GAP`,说明是记录锁; 82 | - 如果 LOCK_MODE 为 `X, GAP`,说明是间隙锁; 83 | 84 | 然后通过 `LOCK_DATA` 信息,可以确认 next-key 锁的范围,具体怎么确定呢? 85 | 86 | - 根据我的经验,如果 LOCK_MODE 是 next-key 锁或者间隙锁,那么 **LOCK_DATA 就表示锁的范围最右值**,而锁范围的最左值为 LOCK_DATA 的上一条记录的值。 87 | 88 | 因此,此时事务 A 在主键索引(`INDEX_NAME : PRIMARY`)上加了 10 个 next-key 锁,如下: 89 | - X 型的 next-key 锁,范围:(-∞, 1] 90 | - X 型的 next-key 锁,范围:(1, 2] 91 | - X 型的 next-key 锁,范围:(2, 3] 92 | - X 型的 next-key 锁,范围:(3, 4] 93 | - X 型的 next-key 锁,范围:(4, 5] 94 | - X 型的 next-key 锁,范围:(5, 6] 95 | - X 型的 next-key 锁,范围:(6, 7] 96 | - X 型的 next-key 锁,范围:(7, 8] 97 | - X 型的 next-key 锁,范围:(8, 9] 98 | - X 型的 next-key 锁,范围:(9, +∞] 99 | 100 | **这相当于把整个表给锁住了,其他事务在对该表进行增、删、改操作的时候都会被阻塞**。 101 | 102 | 只有在事务 A 提交了事务,事务 A 执行过程中产生的锁才会被释放。 103 | 104 | > 为什么只是查询年龄 20 岁以上行记录,而把整个表给锁住了呢? 105 | 106 | 这是因为事务 A 的这条查询语句是**全表扫描,锁是在遍历索引的时候加上的,并不是针对输出的结果加锁**。 107 | 108 | ![](https://img-blog.csdnimg.cn/e0b2a18daa864306a84ec51c0866d170.png) 109 | 110 | 因此,**在线上在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了**,这是挺严重的问题。 111 | 112 | 113 | > 如果对 age 建立索引,事务 A 这条查询会加什么锁呢? 114 | 115 | 接下来,我**对 age 字段建立索引**,然后再执行这条查询语句: 116 | 117 | ![](https://img-blog.csdnimg.cn/68dd89fc95aa42cf9b0c4251d4e9226c.png) 118 | 119 | 120 | 接下来,继续通过 `select * from performance_schema.data_locks\G;` 这条语句,查看事务执行 SQL 过程中加了什么锁。 121 | 122 | 具体的信息,我就不打印了,我直接说结论吧。 123 | 124 | **因为表中有两个索引,分别是主键索引和 age 索引,所以会分别对这两个索引加锁。** 125 | 126 | 主键索引会加如下的锁: 127 | - X 型的记录锁,锁住 id = 2 的记录; 128 | - X 型的记录锁,锁住 id = 3 的记录; 129 | - X 型的记录锁,锁住 id = 5 的记录; 130 | - X 型的记录锁,锁住 id = 6 的记录; 131 | - X 型的记录锁,锁住 id = 7 的记录; 132 | - X 型的记录锁,锁住 id = 8 的记录; 133 | 134 | 分析 age 索引加锁的范围时,要先对 age 字段进行排序。 135 | ![请添加图片描述](https://img-blog.csdnimg.cn/b93b31af4eec416e9f00c2adc1f7d0c1.png) 136 | 137 | age 索引加的锁: 138 | - X 型的 next-key lock,锁住 age 范围 (19, 21] 的记录; 139 | - X 型的 next-key lock,锁住 age 范围 (21, 21] 的记录; 140 | - X 型的 next-key lock,锁住 age 范围 (21, 23] 的记录; 141 | - X 型的 next-key lock,锁住 age 范围 (23, 23] 的记录; 142 | - X 型的 next-key lock,锁住 age 范围 (23, 39] 的记录; 143 | - X 型的 next-key lock,锁住 age 范围 (39, 43] 的记录; 144 | - X 型的 next-key lock,锁住 age 范围 (43, +∞] 的记录; 145 | 146 | 化简一下,**age 索引 next-key 锁的范围是 (19, +∞]。** 147 | 148 | 可以看到,对 age 字段建立了索引后,查询语句是索引查询,并不会全表扫描,因此**不会把整张表给锁住**。 149 | 150 | ![](https://img-blog.csdnimg.cn/2920c60d5a9b42f2a65933fa14761c20.png) 151 | 152 | 总结一下,在对 age 字段建立索引后,事务 A 在执行下面这条查询语句后,主键索引和 age 索引会加下图中的锁。 153 | 154 | ![请添加图片描述](https://img-blog.csdnimg.cn/5b9a2d7a2cd240fea47b938364f0b76a.png) 155 | 156 | 事务 A 加上锁后,事务 B、C、D、E 在执行以下语句都会被阻塞。 157 | 158 | ![请添加图片描述](https://img-blog.csdnimg.cn/46c9b44142f14217b39bd973868e732e.png) 159 | 160 | 161 | ## 总结 162 | 163 | 在 MySQL 的可重复读隔离级别下,针对当前读的语句会对**索引**加记录锁 + 间隙锁,这样可以避免其他事务执行增、删、改时导致幻读的问题。 164 | 165 | 有一点要注意的是,在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。 166 | 167 | 完! 168 | 169 | --- 170 | 171 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 172 | 173 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /mysql/lock/show_lock.md: -------------------------------------------------------------------------------- 1 | # 字节面试:加了什么锁,导致死锁的? 2 | 3 | 大家好,我是小林。 4 | 5 | 之前收到读者面试字节时,被问到一个关于 MySQL 的问题。 6 | 7 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/锁/字节mysql面试题.png) 8 | 9 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/锁/提问.png) 10 | 11 | 如果对 MySQL 加锁机制比较熟悉的同学,应该一眼就能看出**会发生死锁**,但是具体加了什么锁而导致死锁,是需要我们具体分析的。 12 | 13 | 接下来,就跟聊聊上面两个事务执行 SQL 语句的过程中,加了什么锁,从而导致死锁的。 14 | 15 | ## 准备工作 16 | 17 | 先创建一张 t_student 表,假设除了 id 字段,其他字段都是普通字段。 18 | 19 | ```sql 20 | CREATE TABLE `t_student` ( 21 | `id` int NOT NULL, 22 | `no` varchar(255) DEFAULT NULL, 23 | `name` varchar(255) DEFAULT NULL, 24 | `age` int DEFAULT NULL, 25 | `score` int DEFAULT NULL, 26 | PRIMARY KEY (`id`) 27 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 28 | ``` 29 | 30 | 然后,插入相关的数据后,t_student 表中的记录如下: 31 | 32 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/锁/t_student.png) 33 | 34 | ## 开始实验 35 | 36 | 在实验开始前,先说明下实验环境: 37 | 38 | - MySQL 版本:8.0.26 39 | - 隔离级别:可重复读(RR) 40 | 41 | 启动两个事务,按照题目的 SQL 执行顺序,过程如下表格: 42 | 43 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/锁/ab事务死锁.drawio.png) 44 | 45 | 可以看到,事务 A 和 事务 B 都在执行 insert 语句后,都陷入了等待状态(前提没有打开死锁检测),也就是发生了死锁,因为都在相互等待对方释放锁。 46 | 47 | ## 为什么会发生死锁? 48 | 49 | 我们可以通过 `select * from performance_schema.data_locks\G;` 这条语句,查看事务执行 SQL 过程中加了什么锁。 50 | 51 | 接下来,针对每一条 SQL 语句分析具体加了什么锁。 52 | 53 | ### Time 1 阶段加锁分析 54 | 55 | Time 1 阶段,事务 A 执行以下语句: 56 | 57 | ```sql 58 | # 事务 A 59 | mysql> begin; 60 | Query OK, 0 rows affected (0.00 sec) 61 | 62 | mysql> update t_student set score = 100 where id = 25; 63 | Query OK, 0 rows affected (0.01 sec) 64 | Rows matched: 0 Changed: 0 Warnings: 0 65 | ``` 66 | 67 | 然后执行 `select * from performance_schema.data_locks\G;` 这条语句,查看事务 A 此时加了什么锁。 68 | 69 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/锁/事务a的锁.png) 70 | 71 | 从上图可以看到,共加了两个锁,分别是: 72 | 73 | - 表锁:X 类型的意向锁; 74 | - 行锁:X 类型的间隙锁; 75 | 76 | 这里我们重点关注行锁,图中 LOCK_TYPE 中的 RECORD 表示行级锁,而不是记录锁的意思,通过 LOCK_MODE 可以确认是 next-key 锁,还是间隙锁,还是记录锁: 77 | 78 | - 如果 LOCK_MODE 为 `X`,说明是 next-key 锁; 79 | - 如果 LOCK_MODE 为 `X, REC_NOT_GAP`,说明是记录锁; 80 | - 如果 LOCK_MODE 为 `X, GAP`,说明是间隙锁; 81 | 82 | **因此,此时事务 A 在主键索引(INDEX_NAME : PRIMARY)上加的是间隙锁,锁范围是`(20, 30)`**。 83 | 84 | > 间隙锁的范围`(20, 30)` ,是怎么确定的? 85 | 86 | 根据我的经验,如果 LOCK_MODE 是 next-key 锁或者间隙锁,那么 LOCK_DATA 就表示锁的范围最右值,此次的事务 A 的 LOCK_DATA 是 30。 87 | 88 | 然后锁范围的最左值是 t_student 表中 id 为 30 的上一条记录的 id 值,即 20。 89 | 90 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/403f9c1012e84a4c83bfb2fc3990f177.png) 91 | 92 | 因此,间隙锁的范围`(20, 30)`。 93 | 94 | ### Time 2 阶段加锁分析 95 | 96 | Time 2 阶段,事务 B 执行以下语句: 97 | 98 | ```sql 99 | # 事务 B 100 | mysql> begin; 101 | Query OK, 0 rows affected (0.00 sec) 102 | 103 | mysql> update t_student set score = 100 where id = 26; 104 | Query OK, 0 rows affected (0.01 sec) 105 | Rows matched: 0 Changed: 0 Warnings: 0 106 | ``` 107 | 108 | 然后执行 `select * from performance_schema.data_locks\G;` 这条语句,查看事务 B 此时加了什么锁。 109 | 110 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/44277cfefbd6446db861bfb81a1e4a59.png) 111 | 112 | 从上图可以看到,行锁是 X 类型的间隙锁,间隙锁的范围是`(20, 30)`。 113 | 114 | > 事务 A 和 事务 B 的间隙锁范围都是一样的,为什么不会冲突? 115 | 116 | 两个事务的间隙锁之间是相互兼容的,不会产生冲突。 117 | 118 | 在 MySQL 官网上还有一段非常关键的描述: 119 | 120 | *Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from Inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.* 121 | 122 | **间隙锁的意义只在于阻止区间被插入**,因此是可以共存的。**一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁**,共享(S 型)和排他(X 型)的间隙锁是没有区别的,他们相互不冲突,且功能相同。 123 | 124 | ### Time 3 阶段加锁分析 125 | 126 | Time 3,事务 A 插入了一条记录: 127 | 128 | ```sql 129 | # Time 3 阶段,事务 A 插入了一条记录 130 | mysql> insert into t_student(id, no, name, age,score) value (25, 'S0025', 'sony', 28, 90); 131 | /// 阻塞等待...... 132 | ``` 133 | 134 | 此时,事务 A 就陷入了等待状态。 135 | 136 | 然后执行 `select * from performance_schema.data_locks\G;` 这条语句,查看事务 A 在获取什么锁而导致被阻塞。 137 | 138 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/锁/事务a等待中.png) 139 | 140 | 可以看到,事务 A 的状态为等待状态(LOCK_STATUS: WAITING),因为向事务 B 生成的间隙锁(范围 `(20, 30)`)中插入了一条记录,所以事务 A 的插入操作生成了一个插入意向锁(`LOCK_MODE:INSERT_INTENTION`)。 141 | 142 | > 插入意向锁是什么? 143 | 144 | 注意!插入意向锁名字里虽然有意向锁这三个字,但是它并不是意向锁,它属于行级锁,是一种特殊的间隙锁。 145 | 146 | 在 MySQL 的官方文档中有以下重要描述: 147 | 148 | *An Insert intention lock is a type of gap lock set by Insert operations prior to row Insertion. This lock signals the intent to Insert in such a way that multiple transactions Inserting into the same index gap need not wait for each other if they are not Inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to Insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with Insert intention locks prior to obtaining the exclusive lock on the Inserted row, but do not block each other because the rows are nonconflicting.* 149 | 150 | 这段话表明尽管**插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作**。 151 | 152 | 如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。 153 | 154 | 插入意向锁与间隙锁的另一个非常重要的差别是:**尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。所以,插入意向锁和间隙锁之间是冲突的**。 155 | 156 | 另外,我补充一点,插入意向锁的生成时机: 157 | 158 | - 每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态(*PS:MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁*),现象就是 Insert 语句会被阻塞。 159 | 160 | ### Time 4 阶段加锁分析 161 | 162 | Time 4,事务 B 插入了一条记录: 163 | 164 | ```sql 165 | # Time 4 阶段,事务 B 插入了一条记录 166 | mysql> insert into t_student(id, no, name, age,score) value (26, 'S0026', 'ace', 28, 90); 167 | /// 阻塞等待...... 168 | ``` 169 | 170 | 此时,事务 B 就陷入了等待状态。 171 | 172 | 然后执行 `select * from performance_schema.data_locks\G;` 这条语句,查看事务 B 在获取什么锁而导致被阻塞。 173 | 174 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/锁/事务b等待中.png) 175 | 176 | 可以看到,事务 B 在生成插入意向锁时而导致被阻塞,这是因为事务 B 向事务 A 生成的范围为 (20, 30) 的间隙锁插入了一条记录,而插入意向锁和间隙锁是冲突的,所以事务 B 在获取插入意向锁时就陷入了等待状态。 177 | 178 | > 最后回答,为什么会发生死锁? 179 | 180 | 本次案例中,事务 A 和事务 B 在执行完后 update 语句后都持有范围为`(20, 30)`的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:**互斥、占有且等待、不可强占用、循环等待**,因此发生了死锁。 181 | 182 | ## 总结 183 | 184 | 两个事务即使生成的间隙锁的范围是一样的,也不会发生冲突,因为间隙锁目的是为了防止其他事务插入数据,因此间隙锁与间隙锁之间是相互兼容的。 185 | 186 | 在执行插入语句时,如果插入的记录在其他事务持有间隙锁范围内,插入语句就会被阻塞,因为插入语句在碰到间隙锁时,会生成一个插入意向锁,然后插入意向锁和间隙锁之间是互斥的关系。 187 | 188 | 如果两个事务分别向对方持有的间隙锁范围内插入一条记录,而插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:**互斥、占有且等待、不可强占用、循环等待**,因此发生了死锁。 189 | 190 | ## 读者问答 191 | 192 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/f4d4d7fdb9074b098b1077acff698aea.png) 193 | 194 | --- 195 | 196 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 197 | 198 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /mysql/lock/update_index.md: -------------------------------------------------------------------------------- 1 | # update 没加索引会锁全表? 2 | 3 | 大家好,我是小林。 4 | 5 | 昨晚在群划水的时候,看到有位读者说了这么一件事。 6 | 7 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/202e1521bc02411698eb6162cf121114.png) 8 | 9 | 10 | 大概就是,在线上执行一条 update 语句修改数据库数据的时候,where 条件没有带上索引,导致业务直接崩了,被老板教训了一波 11 | 12 | 这次我们就来看看: 13 | - 为什么会发生这种的事故? 14 | - 又该如何避免这种事故的发生? 15 | 16 | 说个前提,接下来说的案例都是基于 InnoDB 存储引擎,且事务的隔离级别是可重复读。 17 | 18 | ## 为什么会发生这种的事故? 19 | 20 | 21 | InnoDB 存储引擎的默认事务隔离级别是「可重复读」,但是在这个隔离级别下,在多个事务并发的时候,会出现幻读的问题,所谓的幻读是指在同一事务下,连续执行两次同样的查询语句,第二次的查询语句可能会返回之前不存在的行。 22 | 23 | 因此 InnoDB 存储引擎自己实现了行锁,通过 next-key 锁(记录锁和间隙锁的组合)来锁住记录本身和记录之间的“间隙”,防止其他事务在这个记录之间插入新的记录,从而避免了幻读现象。 24 | 25 | 26 | 当我们执行 update 语句时,实际上是会对记录加独占锁(X 锁)的,如果其他事务对持有独占锁的记录进行修改时是会被阻塞的。另外,这个锁并不是执行完 update 语句就会释放的,而是会等事务结束时才会释放。 27 | 28 | 在 InnoDB 事务中,对记录加锁带基本单位是 next-key 锁,但是会因为一些条件会退化成间隙锁,或者记录锁。加锁的位置准确的说,锁是加在索引上的而非行上。 29 | 30 | 比如,在 update 语句的 where 条件使用了唯一索引,那么 next-key 锁会退化成记录锁,也就是只会给一行记录加锁。 31 | 32 | 这里举个例子,这里有一张数据库表,其中 id 为主键索引。 33 | 34 | ![](https://img-blog.csdnimg.cn/img_convert/3c3af16e7a948833ccb6409e8b51daf8.png) 35 | 36 | 37 | 假设有两个事务的执行顺序如下: 38 | 39 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/d2326f98cbb34fc09ca4013703251501.png) 40 | 41 | 42 | 可以看到,事务 A 的 update 语句中 where 是等值查询,并且 id 是唯一索引,所以只会对 id = 1 这条记录加锁,因此,事务 B 的更新操作并不会阻塞。 43 | 44 | 45 | 但是,**在 update 语句的 where 条件没有使用索引,就会全表扫描,于是就会对所有记录加上 next-key 锁(记录锁 + 间隙锁),相当于把整个表锁住了**。 46 | 47 | 假设有两个事务的执行顺序如下: 48 | 49 | ![](https://img-blog.csdnimg.cn/img_convert/1aa886fe95e7bc791c296e2d342fa435.png) 50 | 51 | 52 | 可以看到,这次事务 B 的 update 语句被阻塞了。 53 | 54 | 这是因为事务 A 的 update 语句中 where 条件没有索引列,触发了全表扫描,在扫描过程中会对索引加锁,所以全表扫描的场景下,所有记录都会被加锁,也就是这条 update 语句产生了 4 个记录锁和 5 个间隙锁,相当于锁住了全表。 55 | 56 | 57 | ![](https://img-blog.csdnimg.cn/img_convert/63e055617720853f5b64c99576227c09.png) 58 | 59 | 60 | 因此,当在数据量非常大的数据库表执行 update 语句时,如果没有使用索引,就会给全表的加上 next-key 锁,那么锁就会持续很长一段时间,直到事务结束,而这期间除了 `select ... from`语句,其他语句都会被锁住不能执行,业务会因此停滞,接下来等着你的,就是老板的挨骂。 61 | 62 | 那 update 语句的 where 带上索引就能避免全表记录加锁了吗? 63 | 64 | 并不是。 65 | 66 | **关键还得看这条语句在执行过程种,优化器最终选择的是索引扫描,还是全表扫描,如果走了全表扫描,就会对全表的记录加锁了**。 67 | 68 | :::tip 69 | 70 | 网上很多资料说,update 没加锁索引会加表锁,这是不对的。 71 | 72 | Innodb 源码里面在扫描记录的时候,都是针对索引项这个单位去加锁的,update 不带索引就是全表扫扫描,也就是表里的索引项都加锁,相当于锁了整张表,所以大家误以为加了表锁。 73 | 74 | ::: 75 | 76 | 77 | ## 如何避免这种事故的发生? 78 | 79 | 我们可以将 MySQL 里的 `sql_safe_updates` 参数设置为 1,开启安全更新模式。 80 | 81 | > 官方的解释: 82 | > If set to 1, MySQL aborts UPDATE or DELETE statements that do not use a key in the WHERE clause or a LIMIT clause. (Specifically, UPDATE statements must have a WHERE clause that uses a key or a LIMIT clause, or both. DELETE statements must have both.) This makes it possible to catch UPDATE or DELETE statements where keys are not used properly and that would probably change or delete a large number of rows. The default value is 0. 83 | 84 | 大致的意思是,当 sql_safe_updates 设置为 1 时。 85 | 86 | update 语句必须满足如下条件之一才能执行成功: 87 | - 使用 where,并且 where 条件中必须有索引列; 88 | - 使用 limit; 89 | - 同时使用 where 和 limit,此时 where 条件中可以没有索引列; 90 | 91 | delete 语句必须满足以下条件能执行成功: 92 | - 同时使用 where 和 limit,此时 where 条件中可以没有索引列; 93 | 94 | 如果 where 条件带上了索引列,但是优化器最终扫描选择的是全表,而不是索引的话,我们可以使用 `force index([index_name])` 可以告诉优化器使用哪个索引,以此避免有几率锁全表带来的隐患。 95 | 96 | ## 总结 97 | 98 | 不要小看一条 update 语句,在生产机上使用不当可能会导致业务停滞,甚至崩溃。 99 | 100 | 当我们要执行 update 语句的时候,确保 where 条件中带上了索引列,并且在测试机确认该语句是否走的是索引扫描,防止因为扫描全表,而对表中的所有记录加上锁。 101 | 102 | 我们可以打开 MySQL sql_safe_updates 参数,这样可以预防 update 操作时 where 条件没有带上索引列。 103 | 104 | 如果发现即使在 where 条件中带上了索引列,优化器走的还是全表扫描,这时我们就要使用 `force index([index_name])` 可以告诉优化器使用哪个索引。 105 | 106 | 这次就说到这啦,下次要小心点,别再被老板挨骂啦。 107 | 108 | ---- 109 | 110 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 111 | 112 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /mysql/log/README.md: -------------------------------------------------------------------------------- 1 | redo log、binlog、undolog 正在赶稿的路上。。。。。 2 | 3 | -------------------------------------------------------------------------------- /mysql/transaction/phantom.md: -------------------------------------------------------------------------------- 1 | # MySQL 可重复读隔离级别,完全解决幻读了吗? 2 | 3 | 大家好,我是小林。 4 | 5 | 我在[上一篇文章](https://xiaolincoding.com/mysql/transaction/mvcc.html)提到,MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种: 6 | 7 | - 针对**快照读**(普通 select 语句),是**通过 MVCC 方式解决了幻读**,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。 8 | - 针对**当前读**(select ... for update 等语句),是**通过 next-key lock(记录锁 + 间隙锁)方式解决了幻读**,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。 9 | 10 | 这两个解决方案是很大程度上解决了幻读现象,但是还是有个别的情况造成的幻读现象是无法解决的。 11 | 12 | 这次,就跟大家好好聊这个问题。 13 | 14 | ## 什么是幻读? 15 | 16 | 首先来看看 MySQL 文档是怎么定义幻读(Phantom Read)的: 17 | 18 | ***The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.*** 19 | 20 | 翻译:当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行。 21 | 22 | 举个例子,假设一个事务在 T1 时刻和 T2 时刻分别执行了下面查询语句,途中没有执行其他任何语句: 23 | 24 | ```sql 25 | SELECT * FROM t_test WHERE id > 100; 26 | ``` 27 | 28 | 只要 T1 和 T2 时刻执行产生的结果集是不相同的,那就发生了幻读的问题,比如: 29 | 30 | - T1 时间执行的结果是有 5 条行记录,而 T2 时间执行的结果是有 6 条行记录,那就发生了幻读的问题。 31 | - T1 时间执行的结果是有 5 条行记录,而 T2 时间执行的结果是有 4 条行记录,也是发生了幻读的问题。 32 | 33 | ## 快照读是如何避免幻读的? 34 | 35 | 可重复读隔离级是由 MVCC(多版本并发控制)实现的,实现的方式是启动事务后,在执行第一个查询语句后,会创建一个 Read View,**后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的**,即使中途有其他事务插入了新纪录,是查询不出来这条数据的,所以就很好了避免幻读问题。 36 | 37 | 做个实验,数据库表 t_stu 如下,其中 id 为主键。 38 | 39 | ![](https://img-blog.csdnimg.cn/7f9df142b3594daeaaca495abb7133f5.png) 40 | 41 | 然后在可重复读隔离级别下,有两个事务的执行顺序如下: 42 | 43 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/e576e047dccc47d5a59636ea342750b8.png?) 44 | 45 | 从这个实验结果可以看到,即使事务 B 中途插入了一条记录,事务 A 前后两次查询的结果集都是一样的,并没有出现所谓的幻读现象。 46 | 47 | ## 当前读是如何避免幻读的? 48 | 49 | MySQL 里除了普通查询是快照读,其他都是**当前读**,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。 50 | 51 | 这很好理解,假设你要 update 一个记录,另一个事务已经 delete 这条记录并且提交事务了,这样不是会产生冲突吗,所以 update 的时候肯定要知道最新的数据。 52 | 53 | 另外,`select ... for update` 这种查询语句是当前读,每次执行的时候都是读取最新的数据。 54 | 55 | 接下来,我们假设`select ... for update`当前读是不会加锁的(实际上是会加锁的),在做一遍实验。 56 | 57 | ![](https://img-blog.csdnimg.cn/1f872ff92b644b5f81cee2dd9188b199.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_20,color_FFFFFF,t_70,g_se,x_16) 58 | 59 | 60 | 这时候,事务 B 插入的记录,就会被事务 A 的第二条查询语句查询到(因为是当前读),这样就会出现前后两次查询的结果集合不一样,这就出现了幻读。 61 | 62 | 所以,**Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了间隙锁**。 63 | 64 | 假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。 65 | 66 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/锁/gap锁.drawio.png) 67 | 68 | 举个具体例子,场景如下: 69 | 70 | ![](https://img-blog.csdnimg.cn/3af285a8e70f4d4198318057eb955520.png?) 71 | 72 | 事务 A 执行了这面这条锁定读语句后,就在对表中的记录加上 id 范围为 (2, +∞] 的 next-key lock(next-key lock 是间隙锁 + 记录锁的组合)。 73 | 74 | 然后,事务 B 在执行插入语句的时候,判断到插入的位置被事务 A 加了 next-key lock,于是事物 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交了事务。这就避免了由于事务 B 插入新记录而导致事务 A 发生幻读的现象。 75 | 76 | ## 幻读被完全解决了吗? 77 | 78 | **可重复读隔离级别下虽然很大程度上避免了幻读,但是还是没有能完全解决幻读**。 79 | 80 | 我举例一个可重复读隔离级别发生幻读现象的场景。 81 | 82 | ### 第一个发生幻读现象的场景 83 | 84 | 还是以这张表作为例子: 85 | 86 | ![](https://img-blog.csdnimg.cn/7f9df142b3594daeaaca495abb7133f5.png) 87 | 88 | 事务 A 执行查询 id = 5 的记录,此时表中是没有该记录的,所以查询不出来。 89 | 90 | ```sql 91 | # 事务 A 92 | mysql> begin; 93 | Query OK, 0 rows affected (0.00 sec) 94 | 95 | mysql> select * from t_stu where id = 5; 96 | Empty set (0.01 sec) 97 | ``` 98 | 99 | 然后事务 B 插入一条 id = 5 的记录,并且提交了事务。 100 | 101 | ```sql 102 | # 事务 B 103 | mysql> begin; 104 | Query OK, 0 rows affected (0.00 sec) 105 | 106 | mysql> insert into t_stu values(5, '小美', 18); 107 | Query OK, 1 row affected (0.00 sec) 108 | 109 | mysql> commit; 110 | Query OK, 0 rows affected (0.00 sec) 111 | ``` 112 | 113 | 此时,**事务 A 更新 id = 5 这条记录,对没错,事务 A 看不到 id = 5 这条记录,但是他去更新了这条记录,这场景确实很违和,然后再次查询 id = 5 的记录,事务 A 就能看到事务 B 插入的纪录了,幻读就是发生在这种违和的场景**。 114 | 115 | ```sql 116 | # 事务 A 117 | mysql> update t_stu set name = '小林 coding' where id = 5; 118 | Query OK, 1 row affected (0.01 sec) 119 | Rows matched: 1 Changed: 1 Warnings: 0 120 | 121 | mysql> select * from t_stu where id = 5; 122 | +----+--------------+------+ 123 | | id | name | age | 124 | +----+--------------+------+ 125 | | 5 | 小林coding | 18 | 126 | +----+--------------+------+ 127 | 1 row in set (0.00 sec) 128 | ``` 129 | 130 | 整个发生幻读的时序图如下: 131 | 132 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/锁/幻读发生.drawio.png) 133 | 134 | 在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。 135 | 136 | 因为这种特殊现象的存在,所以我们认为 **MySQL Innodb 中的 MVCC 并不能完全避免幻读现象**。 137 | 138 | ### 第二个发生幻读现象的场景 139 | 140 | 除了上面这一种场景会发生幻读现象之外,还有下面这个场景也会发生幻读现象。 141 | 142 | - T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。 143 | - T2 时刻:事务 B 往`t_test`表中插入一个 id= 200 的记录并提交; 144 | - T3 时刻:事务 A 再执行「当前读语句」select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。 145 | 146 | **要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句**,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。 147 | 148 | ## 总结 149 | 150 | MySQL InnoDB 引擎的可重复读隔离级别(默认隔离级),根据不同的查询方式,分别提出了避免幻读的方案: 151 | 152 | - 针对**快照读**(普通 select 语句),是通过 MVCC 方式解决了幻读。 153 | - 针对**当前读**(select ... for update 等语句),是通过 next-key lock(记录锁 + 间隙锁)方式解决了幻读。 154 | 155 | 我举例了两个发生幻读场景的例子。 156 | 157 | 第一个例子:对于快照读,MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。 158 | 159 | 第二个例子:对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。 160 | 161 | 所以,**MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。** 162 | 163 | 要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。 164 | 165 | ---- 166 | 167 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 168 | 169 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 170 | -------------------------------------------------------------------------------- /network/1_base/how_os_deal_network_package.md: -------------------------------------------------------------------------------- 1 | # 2.3 Linux 系统是如何收发网络包的? 2 | 3 | 这次,就围绕一个问题来说。 4 | 5 | **Linux 系统是如何收发网络包的?** 6 | 7 | ## 网络模型 8 | 9 | 为了使得多种设备能通过网络相互通信,和为了解决各种不同设备在网络互联中的兼容性问题,国际标准化组织制定了开放式系统互联通信参考模型(*Open System Interconnection Reference Model*),也就是 OSI 网络模型,该模型主要有 7 层,分别是应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层。 10 | 11 | 每一层负责的职能都不同,如下: 12 | 13 | - 应用层,负责给应用程序提供统一的接口; 14 | - 表示层,负责把数据转换成兼容另一个系统能识别的格式; 15 | - 会话层,负责建立、管理和终止表示层实体之间的通信会话; 16 | - 传输层,负责端到端的数据传输; 17 | - 网络层,负责数据的路由、转发、分片; 18 | - 数据链路层,负责数据的封帧和差错检测,以及 MAC 寻址; 19 | - 物理层,负责在物理网络中传输数据帧; 20 | 21 | 由于 OSI 模型实在太复杂,提出的也只是概念理论上的分层,并没有提供具体的实现方案。 22 | 23 | 事实上,我们比较常见,也比较实用的是四层模型,即 TCP/IP 网络模型,Linux 系统正是按照这套网络模型来实现网络协议栈的。 24 | 25 | TCP/IP 网络模型共有 4 层,分别是应用层、传输层、网络层和网络接口层,每一层负责的职能如下: 26 | 27 | - 应用层,负责向用户提供一组应用程序,比如 HTTP、DNS、FTP 等; 28 | - 传输层,负责端到端的通信,比如 TCP、UDP 等; 29 | - 网络层,负责网络包的封装、分片、路由、转发,比如 IP、ICMP 等; 30 | - 网络接口层,负责网络包在物理网络中的传输,比如网络包的封帧、MAC 寻址、差错检测,以及通过网卡传输网络帧等; 31 | 32 | TCP/IP 网络模型相比 OSI 网络模型简化了不少,也更加易记,它们之间的关系如下图: 33 | 34 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/操作系统/浮点/OSI与TCP.png) 35 | 36 | 不过,我们常说的七层和四层负载均衡,是用 OSI 网络模型来描述的,七层对应的是应用层,四层对应的是传输层。 37 | 38 | --- 39 | 40 | ## Linux 网络协议栈 41 | 42 | 43 | 我们可以把自己的身体比作应用层中的数据,打底衣服比作传输层中的 TCP 头,外套比作网络层中 IP 头,帽子和鞋子分别比作网络接口层的帧头和帧尾。 44 | 45 | 在冬天这个季节,当我们要从家里出去玩的时候,自然要先穿个打底衣服,再套上保暖外套,最后穿上帽子和鞋子才出门,这个过程就好像我们把 TCP 协议通信的网络包发出去的时候,会把应用层的数据按照网络协议栈层层封装和处理。 46 | 47 | 你从下面这张图可以看到,应用层数据在每一层的封装格式。 48 | 49 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/操作系统/浮点/封装.png) 50 | 51 | 其中: 52 | 53 | - 传输层,给应用数据前面增加了 TCP 头; 54 | - 网络层,给 TCP 数据包前面增加了 IP 头; 55 | - 网络接口层,给 IP 数据包前后分别增加了帧头和帧尾; 56 | 57 | 这些新增的头部和尾部,都有各自的作用,也都是按照特定的协议格式填充,这每一层都增加了各自的协议头,那自然网络包的大小就增大了,但物理链路并不能传输任意大小的数据包,所以在以太网中,规定了最大传输单元(MTU)是 `1500` 字节,也就是规定了单次传输的最大 IP 包大小。 58 | 59 | 当网络包超过 MTU 的大小,就会在网络层分片,以确保分片后的 IP 包不会超过 MTU 大小,如果 MTU 越小,需要的分包就越多,那么网络吞吐能力就越差,相反的,如果 MTU 越大,需要的分包就越少,那么网络吞吐能力就越好。 60 | 61 | 知道了 TCP/IP 网络模型,以及网络包的封装原理后,那么 Linux 网络协议栈的样子,你想必猜到了大概,它其实就类似于 TCP/IP 的四层结构: 62 | 63 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/操作系统/浮点/协议栈.png) 64 | 65 | 66 | 从上图的的网络协议栈,你可以看到: 67 | 68 | - 应用程序需要通过系统调用,来跟 Socket 层进行数据交互; 69 | - Socket 层的下面就是传输层、网络层和网络接口层; 70 | - 最下面的一层,则是网卡驱动程序和硬件网卡设备; 71 | 72 | 73 | ## Linux 接收网络包的流程 74 | 75 | 76 | 网卡是计算机里的一个硬件,专门负责接收和发送网络包,当网卡接收到一个网络包后,会通过 DMA 技术,将网络包写入到指定的内存地址,也就是写入到 Ring Buffer,这个是一个环形缓冲区,接着就会告诉操作系统这个网络包已经到达。 77 | 78 | > 那应该怎么告诉操作系统这个网络包已经到达了呢? 79 | 80 | 最简单的一种方式就是触发中断,也就是每当网卡收到一个网络包,就触发一个中断告诉操作系统。 81 | 82 | 但是,这存在一个问题,在高性能网络场景下,网络包的数量会非常多,那么就会触发非常多的中断,要知道当 CPU 收到了中断,就会停下手里的事情,而去处理这些网络包,处理完毕后,才会回去继续其他事情,那么频繁地触发中断,则会导致 CPU 一直没完没了的处理中断,而导致其他任务可能无法继续前进,从而影响系统的整体效率。 83 | 84 | 所以为了解决频繁中断带来的性能开销,Linux 内核在 2.6 版本中引入了 **NAPI 机制**,它是混合「中断和轮询」的方式来接收网络包,它的核心概念就是**不采用中断的方式读取数据**,而是首先采用中断唤醒数据接收的服务程序,然后 `poll` 的方法来轮询数据。 85 | 86 | 因此,当有网络包到达时,会通过 DMA 技术,将网络包写入到指定的内存地址,接着网卡向 CPU 发起硬件中断,当 CPU 收到硬件中断请求后,根据中断表,调用已经注册的中断处理函数。 87 | 88 | 硬件中断处理函数会做如下的事情: 89 | 90 | - 需要先「暂时屏蔽中断」,表示已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知 CPU 了,这样可以提高效率,避免 CPU 不停的被中断。 91 | - 接着,发起「软中断」,然后恢复刚才屏蔽的中断。 92 | 93 | 至此,硬件中断处理函数的工作就已经完成。 94 | 95 | 硬件中断处理函数做的事情很少,主要耗时的工作都交给软中断处理函数了。 96 | 97 | > 软中断的处理 98 | 99 | 内核中的 ksoftirqd 线程专门负责软中断的处理,当 ksoftirqd 内核线程收到软中断后,就会来轮询处理数据。 100 | 101 | ksoftirqd 线程会从 Ring Buffer 中获取一个数据帧,用 sk_buff 表示,从而可以作为一个网络包交给网络协议栈进行逐层处理。 102 | 103 | > 网络协议栈 104 | 105 | 首先,会先进入到网络接口层,在这一层会检查报文的合法性,如果不合法则丢弃,合法则会找出该网络包的上层协议的类型,比如是 IPv4,还是 IPv6,接着再去掉帧头和帧尾,然后交给网络层。 106 | 107 | 到了网络层,则取出 IP 包,判断网络包下一步的走向,比如是交给上层处理还是转发出去。当确认这个网络包要发送给本机后,就会从 IP 头里看看上一层协议的类型是 TCP 还是 UDP,接着去掉 IP 头,然后交给传输层。 108 | 109 | 传输层取出 TCP 头或 UDP 头,根据四元组「源 IP、源端口、目的 IP、目的端口」作为标识,找出对应的 Socket,并把数据放到 Socket 的接收缓冲区。 110 | 111 | 最后,应用层程序调用 Socket 接口,将内核的 Socket 接收缓冲区的数据「拷贝」到应用层的缓冲区,然后唤醒用户进程。 112 | 113 | 至此,一个网络包的接收过程就已经结束了,你也可以从下图左边部分看到网络包接收的流程,右边部分刚好反过来,它是网络包发送的流程。 114 | 115 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/操作系统/浮点/收发流程.png) 116 | 117 | ## Linux 发送网络包的流程 118 | 119 | 如上图的右半部分,发送网络包的流程正好和接收流程相反。 120 | 121 | 122 | 首先,应用程序会调用 Socket 发送数据包的接口,由于这个是系统调用,所以会从用户态陷入到内核态中的 Socket 层,内核会申请一个内核态的 sk_buff 内存,**将用户待发送的数据拷贝到 sk_buff 内存,并将其加入到发送缓冲区**。 123 | 124 | 接下来,网络协议栈从 Socket 发送缓冲区中取出 sk_buff,并按照 TCP/IP 协议栈从上到下逐层处理。 125 | 126 | 如果使用的是 TCP 传输协议发送数据,那么**先拷贝一个新的 sk_buff 副本** ,这是因为 sk_buff 后续在调用网络层,最后到达网卡发送完成的时候,这个 sk_buff 会被释放掉。而 TCP 协议是支持丢失重传的,在收到对方的 ACK 之前,这个 sk_buff 不能被删除。所以内核的做法就是每次调用网卡发送的时候,实际上传递出去的是 sk_buff 的一个拷贝,等收到 ACK 再真正删除。 127 | 128 | 接着,对 sk_buff 填充 TCP 头。这里提一下,sk_buff 可以表示各个层的数据包,在应用层数据包叫 data,在 TCP 层我们称为 segment,在 IP 层我们叫 packet,在数据链路层称为 frame。 129 | 130 | 你可能会好奇,为什么全部数据包只用一个结构体来描述呢?协议栈采用的是分层结构,上层向下层传递数据时需要增加包头,下层向上层数据时又需要去掉包头,如果每一层都用一个结构体,那在层之间传递数据的时候,就要发生多次拷贝,这将大大降低 CPU 效率。 131 | 132 | 于是,为了在层级之间传递数据时,不发生拷贝,只用 sk_buff 一个结构体来描述所有的网络包,那它是如何做到的呢?是通过调整 sk_buff 中 `data` 的指针,比如: 133 | 134 | - 当接收报文时,从网卡驱动开始,通过协议栈层层往上传送数据报,通过增加 skb->data 的值,来逐步剥离协议首部。 135 | - 当要发送报文时,创建 sk_buff 结构体,数据缓存区的头部预留足够的空间,用来填充各层首部,在经过各下层协议时,通过减少 skb->data 的值来增加协议首部。 136 | 137 | 你可以从下面这张图看到,当发送报文时,data 指针的移动过程。 138 | 139 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/sk_buff.jpg) 140 | 141 | 至此,传输层的工作也就都完成了。 142 | 143 | 然后交给网络层,在网络层里会做这些工作:选取路由(确认下一跳的 IP)、填充 IP 头、netfilter 过滤、对超过 MTU 大小的数据包进行分片。处理完这些工作后会交给网络接口层处理。 144 | 145 | 网络接口层会通过 ARP 协议获得下一跳的 MAC 地址,然后对 sk_buff 填充帧头和帧尾,接着将 sk_buff 放到网卡的发送队列中。 146 | 147 | 这一些工作准备好后,会触发「软中断」告诉网卡驱动程序,这里有新的网络包需要发送,驱动程序会从发送队列中读取 sk_buff,将这个 sk_buff 挂到 RingBuffer 中,接着将 sk_buff 数据映射到网卡可访问的内存 DMA 区域,最后触发真实的发送。 148 | 149 | 当数据发送完成以后,其实工作并没有结束,因为内存还没有清理。当发送完成的时候,网卡设备会触发一个硬中断来释放内存,主要是释放 sk_buff 内存和清理 RingBuffer 内存。 150 | 151 | 最后,当收到这个 TCP 报文的 ACK 应答时,传输层就会释放原始的 sk_buff。 152 | 153 | > 发送网络数据的时候,涉及几次内存拷贝操作? 154 | 155 | 第一次,调用发送数据的系统调用的时候,内核会申请一个内核态的 sk_buff 内存,将用户待发送的数据拷贝到 sk_buff 内存,并将其加入到发送缓冲区。 156 | 157 | 第二次,在使用 TCP 传输协议的情况下,从传输层进入网络层的时候,每一个 sk_buff 都会被克隆一个新的副本出来。副本 sk_buff 会被送往网络层,等它发送完的时候就会释放掉,然后原始的 sk_buff 还保留在传输层,目的是为了实现 TCP 的可靠传输,等收到这个数据包的 ACK 时,才会释放原始的 sk_buff。 158 | 159 | 第三次,当 IP 层发现 sk_buff 大于 MTU 时才需要进行。会再申请额外的 sk_buff,并将原来的 sk_buff 拷贝为多个小的 sk_buff。 160 | 161 | ## 总结 162 | 163 | 电脑与电脑之间通常都是通过话网卡、交换机、路由器等网络设备连接到一起,那由于网络设备的异构性,国际标准化组织定义了一个七层的 OSI 网络模型,但是这个模型由于比较复杂,实际应用中并没有采用,而是采用了更为简化的 TCP/IP 模型,Linux 网络协议栈就是按照了该模型来实现的。 164 | 165 | TCP/IP 模型主要分为应用层、传输层、网络层、网络接口层四层,每一层负责的职责都不同,这也是 Linux 网络协议栈主要构成部分。 166 | 167 | 当应用程序通过 Socket 接口发送数据包,数据包会被网络协议栈从上到下进行逐层处理后,才会被送到网卡队列中,随后由网卡将网络包发送出去。 168 | 169 | 而在接收网络包时,同样也要先经过网络协议栈从下到上的逐层处理,最后才会被送到应用程序。 170 | 171 | ---- 172 | 173 | 参考资料: 174 | 175 | - Linux 网络包发送过程:https://mp.weixin.qq.com/s/wThfD9th9e_-YGHJJ3HXNQ 176 | - Linux 网络数据接收流程(TCP)- NAPI:https://wenfh2020.com/2021/12/29/kernel-tcp-receive/ 177 | - Linux 网络 - 数据包接收过程:https://blog.csdn.net/frank_jb/article/details/115841622 178 | 179 | --- 180 | 181 | 哈喽,我是小林,就爱图解计算机基础,如果觉得文章对你有帮助,别忘记关注我哦! 182 | 183 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 184 | 185 | -------------------------------------------------------------------------------- /network/1_base/tcp_ip_model.md: -------------------------------------------------------------------------------- 1 | # 2.1 TCP/IP 网络模型有哪几层? 2 | 3 | 问大家,为什么要有 TCP/IP 网络模型? 4 | 5 | 对于同一台设备上的进程间通信,有很多种方式,比如有管道、消息队列、共享内存、信号等方式,而对于不同设备上的进程间通信,就需要网络通信,而设备是多样性的,所以要兼容多种多样的设备,就协商出了一套**通用的网络协议**。 6 | 7 | 这个网络协议是分层的,每一层都有各自的作用和职责,接下来就根据「TCP/IP 网络模型」分别对每一层进行介绍。 8 | 9 | ## 应用层 10 | 11 | 最上层的,也是我们能直接接触到的就是**应用层**(*Application Layer*),我们电脑或手机使用的应用软件都是在应用层实现。那么,当两个不同设备的应用需要通信的时候,应用就把应用数据传给下一层,也就是传输层。 12 | 13 | 所以,应用层只需要专注于为用户提供应用功能,比如 HTTP、FTP、Telnet、DNS、SMTP 等。 14 | 15 | 应用层是不用去关心数据是如何传输的,就类似于,我们寄快递的时候,只需要把包裹交给快递员,由他负责运输快递,我们不需要关心快递是如何被运输的。 16 | 17 | 而且应用层是工作在操作系统中的用户态,传输层及以下则工作在内核态。 18 | 19 | 20 | ## 传输层 21 | 22 | 应用层的数据包会传给传输层,**传输层**(*Transport Layer*)是为应用层提供网络支持的。 23 | 24 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/https/应用层.png) 25 | 26 | 27 | 在传输层会有两个传输协议,分别是 TCP 和 UDP。 28 | 29 | TCP 的全称叫传输控制协议(*Transmission Control Protocol*),大部分应用使用的正是 TCP 传输层协议,比如 HTTP 应用层协议。TCP 相比 UDP 多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方。 30 | 31 | UDP 相对来说就很简单,简单到只负责发送数据包,不保证数据包是否能抵达对方,但它实时性相对更好,传输效率也高。当然,UDP 也可以实现可靠传输,把 TCP 的特性在应用层上实现就可以,不过要实现一个商用的可靠 UDP 传输协议,也不是一件简单的事情。 32 | 33 | 34 | 应用需要传输的数据可能会非常大,如果直接传输就不好控制,因此当传输层的数据包大小超过 MSS(TCP 最大报文段长度) ,就要将数据包分块,这样即使中途有一个分块丢失或损坏了,只需要重新发送这一个分块,而不用重新发送整个数据包。在 TCP 协议中,我们把每个分块称为一个 **TCP 段**(*TCP Segment*)。 35 | 36 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/https/TCP段.png) 37 | 38 | 当设备作为接收方时,传输层则要负责把数据包传给应用,但是一台设备上可能会有很多应用在接收或者传输数据,因此需要用一个编号将应用区分开来,这个编号就是**端口**。 39 | 40 | 比如 80 端口通常是 Web 服务器用的,22 端口通常是远程登录服务器用的。而对于浏览器(客户端)中的每个标签栏都是一个独立的进程,操作系统会为这些进程分配临时的端口号。 41 | 42 | 由于传输层的报文中会携带端口号,因此接收方可以识别出该报文是发送给哪个应用。 43 | 44 | 45 | ## 网络层 46 | 47 | 48 | 传输层可能大家刚接触的时候,会认为它负责将数据从一个设备传输到另一个设备,事实上它并不负责。 49 | 50 | 实际场景中的网络环节是错综复杂的,中间有各种各样的线路和分叉路口,如果一个设备的数据要传输给另一个设备,就需要在各种各样的路径和节点进行选择,而传输层的设计理念是简单、高效、专注,如果传输层还负责这一块功能就有点违背设计原则了。 51 | 52 | 也就是说,我们不希望传输层协议处理太多的事情,只需要服务好应用即可,让其作为应用间数据传输的媒介,帮助实现应用到应用的通信,而实际的传输功能就交给下一层,也就是**网络层**(*Internet Layer*)。 53 | 54 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/https/网络层.png) 55 | 56 | 网络层最常使用的是 IP 协议(*Internet Protocol*),IP 协议会将传输层的报文作为数据部分,再加上 IP 报头组装成 IP 报文,如果 IP 报文大小超过 MTU(以太网中一般为 1500 字节)就会**再次进行分片**,得到一个即将发送到网络的 IP 报文。 57 | 58 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/计算机网络/键入网址过程/12.jpg) 59 | 60 | 61 | 网络层负责将数据从一个设备传输到另一个设备,世界上那么多设备,又该如何找到对方呢?因此,网络层需要有区分设备的编号。 62 | 63 | 我们一般用 IP 地址给设备进行编号,对于 IPv4 协议,IP 地址共 32 位,分成了四段(比如,192.168.100.1),每段是 8 位。只有一个单纯的 IP 地址虽然做到了区分设备,但是寻址起来就特别麻烦,全世界那么多台设备,难道一个一个去匹配?这显然不科学。 64 | 65 | 因此,需要将 IP 地址分成两种意义: 66 | 67 | - 一个是**网络号**,负责标识该 IP 地址是属于哪个「子网」的; 68 | - 一个是**主机号**,负责标识同一「子网」下的不同主机; 69 | 70 | 怎么分的呢?这需要配合**子网掩码**才能算出 IP 地址 的网络号和主机号。 71 | 72 | 举个例子,比如 10.100.122.0/24,后面的`/24`表示就是 `255.255.255.0` 子网掩码,255.255.255.0 二进制是「11111111-11111111-11111111-00000000」,大家数数一共多少个 1?不用数了,是 24 个 1,为了简化子网掩码的表示,用/24 代替 255.255.255.0。 73 | 74 | 知道了子网掩码,该怎么计算出网络地址和主机地址呢? 75 | 76 | 将 10.100.122.2 和 255.255.255.0 进行**按位与运算**,就可以得到网络号,如下图: 77 | 78 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/IP/16.jpg) 79 | 80 | 将 255.255.255.0 取反后与 IP 地址进行进行**按位与运算**,就可以得到主机号。 81 | 82 | 大家可以去搜索下子网掩码计算器,自己改变下「掩码位」的数值,就能体会到子网掩码的作用了。 83 | 84 | ![子网掩码计算器](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4/网络/子网掩码计算器.png) 85 | 86 | 那么在寻址的过程中,先匹配到相同的网络号(表示要找到同一个子网),才会去找对应的主机。 87 | 88 | 除了寻址能力,IP 协议还有另一个重要的能力就是**路由**。实际场景中,两台设备并不是用一条网线连接起来的,而是通过很多网关、路由器、交换机等众多网络设备连接起来的,那么就会形成很多条网络的路径,因此当数据包到达一个网络节点,就需要通过路由算法决定下一步走哪条路径。 89 | 90 | 路由器寻址工作中,就是要找到目标地址的子网,找到后进而把数据包转发给对应的网络内。 91 | 92 | ![IP 地址的网络号](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/IP/17.jpg) 93 | 94 | 所以,**IP 协议的寻址作用是告诉我们去往下一个目的地该朝哪个方向走,路由则是根据「下一个目的地」选择路径。寻址更像在导航,路由更像在操作方向盘**。 95 | 96 | 97 | ## 网络接口层 98 | 99 | 生成了 IP 头部之后,接下来要交给**网络接口层**(*Link Layer*)在 IP 头部的前面加上 MAC 头部,并封装成数据帧(Data frame)发送到网络上。 100 | 101 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/https/网络接口层.png) 102 | 103 | IP 头部中的接收方 IP 地址表示网络包的目的地,通过这个地址我们就可以判断要将包发到哪里,但在以太网的世界中,这个思路是行不通的。 104 | 105 | 什么是以太网呢?电脑上的以太网接口,Wi-Fi 接口,以太网交换机、路由器上的千兆,万兆以太网口,还有网线,它们都是以太网的组成部分。以太网就是一种在「局域网」内,把附近的设备连接起来,使它们之间可以进行通讯的技术。 106 | 107 | 以太网在判断网络包目的地时和 IP 的方式不同,因此必须采用相匹配的方式才能在以太网中将包发往目的地,而 MAC 头部就是干这个用的,所以,在以太网进行通讯要用到 MAC 地址。 108 | 109 | MAC 头部是以太网使用的头部,它包含了接收方和发送方的 MAC 地址等信息,我们可以通过 ARP 协议获取对方的 MAC 地址。 110 | 111 | 所以说,网络接口层主要为网络层提供「链路级别」传输的服务,负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标识网络上的设备。 112 | 113 | ## 总结 114 | 115 | 116 | 综上所述,TCP/IP 网络通常是由上到下分成 4 层,分别是**应用层,传输层,网络层和网络接口层**。 117 | 118 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/tcpip参考模型.drawio.png) 119 | 120 | 再给大家贴一下每一层的封装格式: 121 | 122 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E6%B5%AE%E7%82%B9/%E5%B0%81%E8%A3%85.png) 123 | 124 | 网络接口层的传输单位是帧(frame),IP 层的传输单位是包(packet),TCP 层的传输单位是段(segment),HTTP 的传输单位则是消息或报文(message)。但这些名词并没有什么本质的区分,可以统称为数据包。 125 | 126 | --- 127 | 128 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 129 | 130 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 131 | 132 | -------------------------------------------------------------------------------- /network/2_http/http3.md: -------------------------------------------------------------------------------- 1 | # 3.7 HTTP/3 强势来袭 2 | 3 | HTTP/3 现在(2022 年 5 月)还没正式推出,不过自 2017 年起,HTTP/3 已经更新到 34 个草案了,基本的特性已经确定下来了,对于包格式可能后续会有变化。 4 | 5 | 所以,这次 HTTP/3 介绍不会涉及到包格式,只说它的特性。 6 | 7 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/http3/HTTP3提纲.png) 8 | 9 | ## 美中不足的 HTTP/2 10 | 11 | HTTP/2 通过头部压缩、二进制编码、多路复用、服务器推送等新特性大幅度提升了 HTTP/1.1 的性能,而美中不足的是 HTTP/2 协议是基于 TCP 实现的,于是存在的缺陷有三个。 12 | 13 | - 队头阻塞; 14 | - TCP 与 TLS 的握手时延迟; 15 | - 网络迁移需要重新连接; 16 | 17 | ### 队头阻塞 18 | 19 | HTTP/2 多个请求是跑在一个 TCP 连接中的,那么当 TCP 丢包时,整个 TCP 都要等待重传,那么就会阻塞该 TCP 连接中的所有请求。 20 | 21 | 比如下图中,Stream 2 有一个 TCP 报文丢失了,那么即使收到了 Stream 3 和 Stream 4 的 TCP 报文,应用层也是无法读取的,相当于阻塞了 Stream 3 和 Stream 4 请求。 22 | 23 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/network/quic/http2阻塞.jpeg) 24 | 25 | 因为 TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 视角看,就是请求被阻塞了。 26 | 27 | 举个例子,如下图: 28 | 29 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/http3/tcp队头阻塞.gif) 30 | 31 | 图中发送方发送了很多个 Packet,每个 Packet 都有自己的序号,你可以认为是 TCP 的序列号,其中 Packet 3 在网络中丢失了,即使 Packet 4-6 被接收方收到后,由于内核中的 TCP 数据不是连续的,于是接收方的应用层就无法从内核中读取到,只有等到 Packet 3 重传后,接收方的应用层才可以从内核中读取到数据,这就是 HTTP/2 的队头阻塞问题,是在 TCP 层面发生的。 32 | 33 | ### TCP 与 TLS 的握手时延迟 34 | 35 | 发起 HTTP 请求时,需要经过 TCP 三次握手和 TLS 四次握手(TLS 1.2)的过程,因此共需要 3 个 RTT 的时延才能发出请求数据。 36 | 37 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/http3/TCP%2BTLS.gif) 38 | 39 | 另外,TCP 由于具有「拥塞控制」的特性,所以刚建立连接的 TCP 会有个「慢启动」的过程,它会对 TCP 连接产生“减速”效果。 40 | 41 | ### 网络迁移需要重新连接 42 | 43 | 一个 TCP 连接是由四元组(源 IP 地址,源端口,目标 IP 地址,目标端口)确定的,这意味着如果 IP 地址或者端口变动了,就会导致需要 TCP 与 TLS 重新握手,这不利于移动设备切换网络的场景,比如 4G 网络环境切换成 WiFi。 44 | 45 | 这些问题都是 TCP 协议固有的问题,无论应用层的 HTTP/2 在怎么设计都无法逃脱。要解决这个问题,就必须把**传输层协议替换成 UDP**,这个大胆的决定,HTTP/3 做了! 46 | 47 | ![HTTP/1 ~ HTTP/3](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/计算机网络/HTTP/27-HTTP3.png) 48 | 49 | ## QUIC 协议的特点 50 | 51 | 我们深知,UDP 是一个简单、不可靠的传输协议,而且是 UDP 包之间是无序的,也没有依赖关系。 52 | 53 | 而且,UDP 是不需要连接的,也就不需要握手和挥手的过程,所以天然的就比 TCP 快。 54 | 55 | 当然,HTTP/3 不仅仅只是简单将传输协议替换成了 UDP,还基于 UDP 协议在「应用层」实现了 **QUIC 协议**,它具有类似 TCP 的连接管理、拥塞窗口、流量控制的网络特性,相当于将不可靠传输的 UDP 协议变成“可靠”的了,所以不用担心数据包丢失的问题。 56 | 57 | QUIC 协议的优点有很多,这里举例几个,比如: 58 | 59 | - 无队头阻塞; 60 | - 更快的连接建立; 61 | - 连接迁移; 62 | 63 | 64 | ### 无队头阻塞 65 | 66 | QUIC 协议也有类似 HTTP/2 Stream 与多路复用的概念,也是可以在同一条连接上并发传输多个 Stream,Stream 可以认为就是一条 HTTP 请求。 67 | 68 | 由于 QUIC 使用的传输协议是 UDP,UDP 不关心数据包的顺序,如果数据包丢失,UDP 也不关心。 69 | 70 | 不过 QUIC 协议会保证数据包的可靠性,每个数据包都有一个序号唯一标识。当某个流中的一个数据包丢失了,即使该流的其他数据包到达了,数据也无法被 HTTP/3 读取,直到 QUIC 重传丢失的报文,数据才会交给 HTTP/3。 71 | 72 | 而其他流的数据报文只要被完整接收,HTTP/3 就可以读取到数据。这与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。 73 | 74 | 所以,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,某个流发生丢包了,只会影响该流,其他流不受影响。 75 | 76 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/network/quic/quic无阻塞.jpeg) 77 | 78 | ### 更快的连接建立 79 | 80 | 81 | 对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、OpenSSL 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP 握手,再 TLS 握手。 82 | 83 | HTTP/3 在传输数据前虽然需要 QUIC 协议握手,这个握手过程只需要 1 RTT,握手的目的是为确认双方的「连接 ID」,连接迁移就是基于连接 ID 实现的。 84 | 85 | 但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层,而是 **QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS 1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果**。 86 | 87 | 如下图右边部分,HTTP/3 当会话恢复时,有效负载数据与第一个数据包一起发送,可以做到 0-RTT: 88 | 89 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/http3/0-rtt.gif) 90 | 91 | 92 | ### 连接迁移 93 | 94 | 在前面我们提到,基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接。 95 | 96 | ![TCP 四元组](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L2doL3hpYW9saW5jb2Rlci9JbWFnZUhvc3QyLyVFOCVBRSVBMSVFNyVBRSU5NyVFNiU5QyVCQSVFNyVCRCU5MSVFNyVCQiU5Qy9UQ1AtJUU0JUI4JTg5JUU2JUFDJUExJUU2JThGJUExJUU2JTg5JThCJUU1JTkyJThDJUU1JTlCJTlCJUU2JUFDJUExJUU2JThDJUE1JUU2JTg5JThCLzEwLmpwZw?x-oss-process=image/format,png) 97 | 98 | 那么当移动设备的网络从 4G 切换到 WiFi 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立连接,而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。 99 | 100 | 101 | 而 QUIC 协议没有用四元组的方式来“绑定”连接,而是通过**连接 ID** 来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了**连接迁移**的功能。 102 | 103 | ## HTTP/3 协议 104 | 105 | 了解完 QUIC 协议的特点后,我们再来看看 HTTP/3 协议在 HTTP 这一层做了什么变化。 106 | 107 | HTTP/3 同 HTTP/2 一样采用二进制帧的结构,不同的地方在于 HTTP/2 的二进制帧里需要定义 Stream,而 HTTP/3 自身不需要再定义 Stream,直接使用 QUIC 里的 Stream,于是 HTTP/3 的帧的结构也变简单了。 108 | 109 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/http3/http3frame.png) 110 | 111 | 从上图可以看到,HTTP/3 帧头只有两个字段:类型和长度。 112 | 113 | 114 | 根据帧类型的不同,大体上分为数据帧和控制帧两大类,Headers 帧(HTTP 头部)和 DATA 帧(HTTP 包体)属于数据帧。 115 | 116 | 117 | HTTP/3 在头部压缩算法这一方面也做了升级,升级成了 **QPACK**。与 HTTP/2 中的 HPACK 编码方式相似,HTTP/3 中的 QPACK 也采用了静态表、动态表及 Huffman 编码。 118 | 119 | 对于静态表的变化,HTTP/2 中的 HPACK 的静态表只有 61 项,而 HTTP/3 中的 QPACK 的静态表扩大到 91 项。 120 | 121 | HTTP/2 和 HTTP/3 的 Huffman 编码并没有多大不同,但是动态表编解码方式不同。 122 | 123 | 所谓的动态表,在首次请求 - 响应后,双方会将未包含在静态表中的 Header 项更新各自的动态表,接着后续传输时仅用 1 个数字表示,然后对方可以根据这 1 个数字从动态表查到对应的数据,就不必每次都传输长长的数据,大大提升了编码效率。 124 | 125 | 可以看到,**动态表是具有时序性的,如果首次出现的请求发生了丢包,后续的收到请求,对方就无法解码出 HPACK 头部,因为对方还没建立好动态表,因此后续的请求解码会阻塞到首次请求中丢失的数据包重传过来**。 126 | 127 | 128 | HTTP/3 的 QPACK 解决了这一问题,那它是如何解决的呢? 129 | 130 | QUIC 会有两个特殊的单向流,所谓的单向流只有一端可以发送消息,双向则指两端都可以发送消息,传输 HTTP 消息时用的是双向流,这两个单向流的用法: 131 | 132 | - 一个叫 QPACK Encoder Stream,用于将一个字典(Key-Value)传递给对方,比如面对不属于静态表的 HTTP 请求头部,客户端可以通过这个 Stream 发送字典; 133 | - 一个叫 QPACK Decoder Stream,用于响应对方,告诉它刚发的字典已经更新到自己的本地动态表了,后续就可以使用这个字典来编码了。 134 | 135 | 这两个特殊的单向流是用来**同步双方的动态表**,编码方收到解码方更新确认的通知后,才使用动态表编码 HTTP 头部。 136 | 137 | ## 总结 138 | 139 | HTTP/2 虽然具有多个流并发传输的能力,但是传输层是 TCP 协议,于是存在以下缺陷: 140 | 141 | - **队头阻塞**,HTTP/2 多个请求跑在一个 TCP 连接中,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 视角看,就是多个请求被阻塞了; 142 | - **TCP 和 TLS 握手时延**,TCP 三次握手和 TLS 四次握手,共有 3-RTT 的时延; 143 | - **连接迁移需要重新连接**,移动设备从 4G 网络环境切换到 WiFi 时,由于 TCP 是基于四元组来确认一条 TCP 连接的,那么网络环境变化后,就会导致 IP 地址或端口变化,于是 TCP 只能断开连接,然后再重新建立连接,切换网络环境的成本高; 144 | 145 | HTTP/3 就将传输层从 TCP 替换成了 UDP,并在 UDP 协议上开发了 QUIC 协议,来保证数据的可靠传输。 146 | 147 | QUIC 协议的特点: 148 | 149 | - **无队头阻塞**,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,也不会有底层协议限制,某个流发生丢包了,只会影响该流,其他流不受影响; 150 | - **建立连接速度快**,因为 QUIC 内部包含 TLS 1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与 TLS 密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。 151 | - **连接迁移**,QUIC 协议没有用四元组的方式来“绑定”连接,而是通过「连接 ID」来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本; 152 | 153 | 另外 HTTP/3 的 QPACK 通过两个特殊的单向流来同步双方的动态表,解决了 HTTP/2 的 HPACK 队头阻塞问题。 154 | 155 | **期待,HTTP/3 正式推出的那一天!** 156 | 157 | --- 158 | 159 | 参考资料: 160 | 161 | 1. https://medium.com/faun/http-2-spdy-and-http-3-quic-bae7d9a3d484 162 | 2. https://developers.google.com/web/fundamentals/performance/http2?hl=zh-cn 163 | 3. https://blog.cloudflare.com/http3-the-past-present-and-future/ 164 | 4. https://tools.ietf.org/html/draft-ietf-quic-http-34 165 | 5. https://tools.ietf.org/html/draft-ietf-quic-transport-34#section-17 166 | 6. https://ably.com/topic/http3?amp%3Butm_campaign=evergreen&%3Butm_source=reddit&utm_medium=referral 167 | 7. https://www.nginx.org.cn/article/detail/422 168 | 8. https://www.bilibili.com/read/cv793000/ 169 | 9. https://www.chinaz.com/2020/1009/1192436.shtml 170 | 171 | 172 | --- 173 | 174 | 哈喽,我是小林,就爱图解计算机基础,如果文章对你有帮助,别忘记关注哦! 175 | 176 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 177 | 178 | -------------------------------------------------------------------------------- /network/3_tcp/isn_deff.md: -------------------------------------------------------------------------------- 1 | # 4.7 为什么 TCP 每次建立连接时,初始化序列号都要不一样呢? 2 | 3 | 大家好,我是小林。 4 | 5 | **为什么 TCP 每次建立连接时,初始化序列号都要不一样呢?** 6 | 7 | 接下来,我一步一步给大家讲明白,我觉得应该有不少人会有类似的问题,所以今天再肝一篇! 8 | 9 | > 为什么 TCP 每次建立连接时,初始化序列号都要不一样呢? 10 | 11 | 主要原因是为了防止历史报文被下一个相同四元组的连接接收。 12 | 13 | > TCP 四次挥手中的 TIME_WAIT 状态不是会持续 2 MSL 时长,历史报文不是早就在网络中消失了吗? 14 | 15 | 是的,如果能正常四次挥手,由于 TIME_WAIT 状态会持续 2 MSL 时长,历史报文会在下一个连接之前就会自然消失。 16 | 17 | 但是来了,我们并不能保证每次连接都能通过四次挥手来正常关闭连接。 18 | 19 | 假设每次建立连接,客户端和服务端的初始化序列号都是从 0 开始: 20 | 21 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/network/tcp/isn相同.png) 22 | 23 | 过程如下: 24 | 25 | - 客户端和服务端建立一个 TCP 连接,在客户端发送数据包被网络阻塞了,然后超时重传了这个数据包,而此时服务端设备断电重启了,之前与客户端建立的连接就消失了,于是在收到客户端的数据包的时候就会发送 RST 报文。 26 | - 紧接着,客户端又与服务端建立了与上一个连接相同四元组的连接; 27 | - 在新连接建立完成后,上一个连接中被网络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号正好是在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据错乱。 28 | 29 | 可以看到,如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。 30 | 31 | > 客户端和服务端的初始化序列号不一样不是也会发生这样的事情吗? 32 | 33 | 是的,即使客户端和服务端的初始化序列号不一样,也会存在收到历史报文的可能。 34 | 35 | 但是我们要清楚一点,历史报文能否被对方接收,还要看该历史报文的序列号是否正好在对方接收窗口内,如果不在就会丢弃,如果在才会接收。 36 | 37 | 如果每次建立连接客户端和服务端的初始化序列号都「不一样」,就有大概率因为历史报文的序列号「不在」对方接收窗口,从而很大程度上避免了历史报文,比如下图: 38 | 39 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/network/tcp/isn不相同.png) 40 | 41 | 相反,如果每次建立连接客户端和服务端的初始化序列号都「一样」,就有大概率遇到历史报文的序列号刚「好在」对方的接收窗口内,从而导致历史报文被新连接成功接收。 42 | 43 | 所以,每次初始化序列号不一样能够很大程度上避免历史报文被下一个相同四元组的连接接收,注意是很大程度上,并不是完全避免了。 44 | 45 | > 那客户端和服务端的初始化序列号都是随机的,那还是有可能随机成一样的呀? 46 | 47 | RFC793 提到初始化序列号 ISN 随机生成算法:`ISN = M + F(localhost, localport, remotehost, remoteport)`。 48 | 49 | - M 是一个计时器,这个计时器每隔 4 微秒加 1。 50 | - F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值,要保证 Hash 算法不能被外部轻易推算得出。 51 | 52 | 可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。 53 | 54 | > 懂了,客户端和服务端初始化序列号都是随机生成的话,就能避免连接接收历史报文了。 55 | 56 | 是的,但是也不是完全避免了。 57 | 58 | 为了能更好的理解这个原因,我们先来了解序列号(SEQ)和初始序列号(ISN)。 59 | 60 | - **序列号**,是 TCP 一个头部字段,标识了 TCP 发送端到 TCP 接收端的数据流的一个字节,因为 TCP 是面向字节流的可靠协议,为了保证消息的顺序性和可靠性,TCP 为每个传输方向上的每个字节都赋予了一个编号,以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序。**序列号是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0**。 61 | - **初始序列号**,在 TCP 建立连接的时候,客户端和服务端都会各自生成一个初始序列号,它是基于时钟生成的一个随机数,来保证每个连接都拥有不同的初始序列号。**初始化序列号可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时**。 62 | 63 | 给大家抓了一个包,下图中的 Seq 就是序列号,其中红色框住的分别是客户端和服务端各自生成的初始序列号。 64 | 65 | ![](https://img-blog.csdnimg.cn/img_convert/ed84bb4aa742a33f50d8035da2867ca2.png) 66 | 67 | 通过前面我们知道,**序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据**。 68 | 69 | 不要以为序列号的上限值是 4 GB,就以为很大,很难发生回绕。在一个速度足够快的网络中传输大量数据时,序列号的回绕时间就会变短。如果序列号回绕的时间极短,我们就会再次面临之前延迟的报文抵达后序列号依然有效的问题。 70 | 71 | 为了解决这个问题,就需要有 TCP 时间戳。`tcp_timestamps` 参数是默认开启的,开启了 `tcp_timestamps` 参数,TCP 头部就会使用时间戳选项,它有两个好处,**一个是便于精确计算 RTT,另一个是能防止序列号回绕(PAWS)**。 72 | 73 | 试看下面的示例,假设 TCP 的发送窗口是 1 GB,并且使用了时间戳选项,发送方会为每个 TCP 报文分配时间戳数值,我们假设每个报文时间加 1,然后使用这个连接传输一个 6 GB 大小的数据流。 74 | 75 | ![图片](https://img-blog.csdnimg.cn/img_convert/1d497c38621ebc44ee3d8763fd03da67.png) 76 | 77 | 32 位的序列号在时刻 D 和 E 之间回绕。假设在时刻 B 有一个报文丢失并被重传,又假设这个报文段在网络上绕了远路并在时刻 F 重新出现。如果 TCP 无法识别这个绕回的报文,那么数据完整性就会遭到破坏。 78 | 79 | 使用时间戳选项能够有效的防止上述问题,如果丢失的报文会在时刻 F 重新出现,由于它的时间戳为 2,小于最近的有效时间戳(5 或 6),因此防回绕序列号算法(PAWS)会将其丢弃。 80 | 81 | 防回绕序列号算法要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,**如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包**。 82 | 83 | > 懂了,客户端和服务端的初始化序列号都是随机生成,能很大程度上避免历史报文被下一个相同四元组的连接接收,然后又引入时间戳的机制,从而完全避免了历史报文被接收的问题。 84 | 85 | 嗯嗯,没错。 86 | 87 | > 如果时间戳也回绕了怎么办? 88 | 89 | 时间戳的大小是 32 bit,所以理论上也是有回绕的可能性的。 90 | 91 | 时间戳回绕的速度只与对端主机时钟频率有关。 92 | 93 | Linux 以本地时钟计数(jiffies)作为时间戳的值,不同的增长时间会有不同的问题: 94 | 95 | - 如果时钟计数加 1 需要 1 ms,则需要约 24.8 天才能回绕一半,只要报文的生存时间小于这个值的话判断新旧数据就不会出错。 96 | - 如果时钟计数提高到 1 us 加 1,则回绕需要约 71.58 分钟才能回绕,这时问题也不大,因为网络中旧报文几乎不可能生存超过 70 分钟,只是如果 70 分钟没有报文收发则会有一个包越过 PAWS(这种情况会比较多见,相比之下 24 天没有数据传输的 TCP 连接少之又少),但除非这个包碰巧是序列号回绕的旧数据包而被放入接收队列(太巧了吧),否则也不会有问题; 97 | - 如果时钟计数提高到 0.1 us 加 1 回绕需要 7 分钟多一点,这时就可能会有问题了,连接如果 7 分钟没有数据收发就会有一个报文越过 PAWS,对于 TCP 连接而言这么短的时间内没有数据交互太常见了吧!这样的话会频繁有包越过 PAWS 检查,从而使得旧包混入数据中的概率大大增加; 98 | 99 | Linux 在 PAWS 检查做了一个特殊处理,如果一个 TCP 连接连续 24 天不收发数据则在接收第一个包时基于时间戳的 PAWS 会失效,也就是可以 PAWS 函数会放过这个特殊的情况,认为是合法的,可以接收该数据包。 100 | 101 | ```c 102 | // tcp_paws_check 函数如果返回 true 则 PAWS 通过: 103 | static inline bool tcp_paws_check(const struct tcp_options_received *rx_opt, int paws_win) 104 | { 105 | ...... 106 | 107 | // 从上次收到包到现在经历的时间多于 24 天,返回 true 108 | if (unlikely(get_seconds() >= rx_opt->ts_recent_stamp + TCP_PAWS_24DAYS)) 109 | return true; 110 | 111 | ..... 112 | return false; 113 | } 114 | ``` 115 | 116 | 要解决时间戳回绕的问题,可以考虑以下解决方案: 117 | 118 | 1)增加时间戳的大小,由 32 bit 扩大到 64 bit 119 | 120 | 这样虽然可以在能够预见的未来解决时间戳回绕的问题,但会导致新旧协议兼容性问题,像现在的 IPv4 与 IPv6 一样 121 | 122 | 2)将一个与时钟频率无关的值作为时间戳,时钟频率可以增加但时间戳的增速不变 123 | 124 | 随着时钟频率的提高,TCP 在相同时间内能够收发的包也会越来越多。如果时间戳的增速不变,则会有越来越多的报文使用相同的时间戳。这种趋势到达一定程度则时间戳就会失去意义,除非在可预见的未来这种情况不会发生。 125 | 126 | 3)暂时没想到 127 | 128 | --- 129 | 130 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 131 | 132 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /network/3_tcp/out_of_order_fin.md: -------------------------------------------------------------------------------- 1 | # 4.10 四次挥手中收到乱序的 FIN 包会如何处理? 2 | 3 | 大家好,我是小林。 4 | 5 | 收到个读者的问题,他在面试鹅厂的时候,被搞懵了,因为面试官问了他这么一个网络问题: 6 | 7 | ![](https://img-blog.csdnimg.cn/39f790ee7a45473587c8fe3e08e01ba4.jpg?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_17,color_FFFFFF,t_70,g_se,x_16) 8 | 9 | 不得不说,鹅厂真的很喜欢问网络问题,而且爱问异常情况下的网络问题,之前也有篇另外一个读者面试鹅厂的网络问题:「[被鹅厂面怕了!](https://blog.csdn.net/qq_34827674/article/details/117922761)」。 10 | 11 | 12 | 不过这道鹅厂的网络题可能是提问的读者表述有问题,**因为如果 FIN 报文比数据包先抵达客户端,此时 FIN 报文其实是一个乱序的报文,此时客户端的 TCP 连接并不会从 FIN_WAIT_2 状态转换到 TIME_WAIT 状态**。 13 | 14 | ![](https://img-blog.csdnimg.cn/ccabc2f21b014c6c9118cd29ae11c18c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_20,color_FFFFFF,t_70,g_se,x_16) 15 | 16 | 因此,我们要关注到点是看「**在 FIN_WAIT_2 状态下,是如何处理收到的乱序到 FIN 报文,然后 TCP 连接又是什么时候才进入到 TIME_WAIT 状态?**」。 17 | 18 | 我这里先直接说结论: 19 | 20 | **在 FIN_WAIT_2 状态时,如果收到乱序的 FIN 报文,那么就被会加入到「乱序队列」,并不会进入到 TIME_WAIT 状态。** 21 | 22 | **等再次收到前面被网络延迟的数据包时,会判断乱序队列有没有数据,然后会检测乱序队列中是否有可用的数据,如果能在乱序队列中找到与当前报文的序列号保持的顺序的报文,就会看该报文是否有 FIN 标志,如果发现有 FIN 标志,这时才会进入 TIME_WAIT 状态。** 23 | 24 | 我也画了一张图,大家可以结合着图来理解。 25 | 26 | ![](https://img-blog.csdnimg.cn/4effcf2a9e7e4adeb892da98ee21694b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_20,color_FFFFFF,t_70,g_se,x_16) 27 | ## TCP 源码分析 28 | 接下来,我带大家看看源码,听到要源码分析,可能有的同学就怂了。 29 | 30 | 其实要分析我们今天这个问题,只要懂 if else 就行了,我也会用中文来表述代码的逻辑,所以单纯看我的文字也是可以的。 31 | 32 | 这次我们重点分析的是,在 FIN_WAIT_2 状态下,收到 FIN 报文是如何处理的。 33 | 34 | 在 Linux 内核里,当 IP 层处理完消息后,会通过回调 tcp_v4_rcv 函数将消息转给 TCP 层,所以这个函数就是 TCP 层收到消息的入口。 35 | 36 | ![](https://img-blog.csdnimg.cn/ad39a3204f914df89aa6c6138cfc31aa.jpg?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_20,color_FFFFFF,t_70,g_se,x_16) 37 | 处于 FIN_WAIT_2 状态下的客户端,在收到服务端的报文后,最终会调用 tcp_v4_do_rcv 函数。 38 | 39 | 40 | ![](https://img-blog.csdnimg.cn/c5ca5b3fea0e4ad6baa2ab370358f03e.jpg?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_20,color_FFFFFF,t_70,g_se,x_16) 41 | 42 | 接下来,tcp_v4_do_rcv 方法会调用 tcp_rcv_state_process,在这里会根据 TCP 状态做对应的处理,这里我们只关注 FIN_WAIT_2 状态。 43 | 44 | ![](https://img-blog.csdnimg.cn/f76b7e2167544fec859700f55138e95f.jpg?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_20,color_FFFFFF,t_70,g_se,x_16) 45 | 46 | 在上面这个代码里,可以看到如果 shutdown 关闭了读方向,那么在收到对方发来的数据包,则会回复 RST 报文。 47 | 48 | 而我们这次的题目里,shutdown 只关闭了写方向,所以会继续往下调用 tcp_data_queue 函数(因为 case TCP_FIN_WAIT2 代码块里并没有 break 语句,所以会走到该函数)。 49 | 50 | ![](https://img-blog.csdnimg.cn/4ff161a34408447fa38b120b014b29f4.jpg?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_20,color_FFFFFF,t_70,g_se,x_16) 51 | 在上面的 tcp_data_queue 函数里,如果收到的报文的序列号是我们预期的,也就是有序的话: 52 | - 会判断该报文有没有 FIN 标志,如果有的话就会调用 tcp_fin 函数,这个函数负责将 FIN_WAIT_2 状态转换为 TIME_WAIT。 53 | - 接着还会看乱序队列有没有数据,如果有的话会调用 tcp_ofo_queue 函数,这个函数负责检查乱序队列中是否有数据包可用,即能不能在乱序队列找到与当前数据包保持序列号连续的数据包。 54 | 55 | 而当收到的报文的序列号不是我们预期的,也就是乱序的话,则调用 tcp_data_queue_ofo 函数,将报文加入到乱序队列,这个队列的数据结构是红黑树。 56 | 57 | 我们的题目里,客户端收到的 FIN 报文实际上是一个乱序的报文,因此此时并不会调用 tcp_fin 函数进行状态转换,而是将报文通过 tcp_data_queue_ofo 函数加入到乱序队列。 58 | 59 | 然后当客户端收到被网络延迟的数据包后,此时因为该数据包的序列号是期望的,然后又因为上一次收到的乱序 FIN 报文被加入到了乱序队列,表明乱序队列是有数据的,于是就会调用 tcp_ofo_queue 函数。 60 | 61 | 我们来看看 tcp_ofo_queue 函数。 62 | 63 | ![](https://img-blog.csdnimg.cn/dd51b407245d45549eeae64d24634133.jpg?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_20,color_FFFFFF,t_70,g_se,x_16) 64 | 65 | 在上面的 tcp_ofo_queue 函数里,在乱序队列中找到能与当前报文的序列号保持的顺序的报文后,会看该报文是否有 FIN 标志,如果有的话,就会调用 tcp_fin() 函数。 66 | 67 | 最后,我们来看看 tcp_fin 函数的处理。 68 | 69 | ![](https://img-blog.csdnimg.cn/67b33007fcd04d2fa98e79d19823fc95.jpg?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_20,color_FFFFFF,t_70,g_se,x_16) 70 | 71 | 可以看到,如果当前的 TCP 状态为 TCP_FIN_WAIT2,就会发送第四次挥手 ack,然后调用 tcp_time_wait 函数,这个函数里会将 TCP 状态变更为 TIME_WAIT,并启动 TIME_WAIT 的定时器。 72 | 73 | ## 怎么看 TCP 源码? 74 | 之前有不少同学问我,我是怎么看 TCP 源码的? 75 | 76 | 其实我看 TCP 源码,并不是直接打开 Linux 源码直接看,因为 Linux 源码实在太庞大了,如果我不知道 TCP 入口函数在哪,那简直就是大海捞针。 77 | 78 | 79 | 80 | 所以,在看 TCP 源码,我们可以去网上搜索下别人的源码分析,网上已经有很多前辈帮我们分析了 TCP 源码了,而且各个函数的调用链路,他们都有写出来了。 81 | 82 | 83 | 比如,你想了解 TCP 三次握手/四次挥手的源码实现,你就可以以「TCP 三次握手/四次挥手的源码分析」这样关键字来搜索,大部分文章的注释写的还是很清晰,我最开始就按这种方式来学习 TCP 源码的。 84 | 85 | 网上的文章一般只会将重点的部分,很多代码细节没有贴出来,如果你想完整的看到函数的所有代码,那就得看内核代码了。 86 | 87 | 88 | 这里推荐个看 Linux 内核代码的在线网站: 89 | 90 | https://elixir.bootlin.com/linux/latest/source 91 | 92 | ![](https://img-blog.csdnimg.cn/c56e69f998e747208abb82897edc2629.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bCP5p6XY29kaW5n,size_20,color_FFFFFF,t_70,g_se,x_16) 93 | 94 | 95 | 我觉得还是挺好用的,左侧各个版本的代码都有,右上角也可以搜索函数。 96 | 97 | 所以,我看 TCP 源码的经验就是,先在网上找找前辈写的 TCP 源码分析,然后知道整个函数的调用链路后,如果想具体了解某个函数的具体实现,可以在我说的那个看 Linux 内核代码的在线网站上搜索该函数,就可以看到完整的函数的实现。如果中途遇到看不懂的代码,也可以将这个代码复制到百度或者谷歌搜索,一般也能找到别人分析的过程。 98 | 99 | 学会了看 TCP 源码其实有助于我们分析一些异常问题,就比如今天这道网络题目,在网上其实是搜索不出答案的,而且我们也很难用实验的方式来模拟。 100 | 101 | 所以要想知道答案,只能去看源码。 102 | 103 | --- 104 | 105 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 106 | 107 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /network/3_tcp/syn_drop.md: -------------------------------------------------------------------------------- 1 | # 4.8 SYN 报文什么时候情况下会被丢弃? 2 | 3 | 大家好,我是小林。 4 | 5 | 之前有个读者在秋招面试的时候,被问了这么一个问题:SYN 报文什么时候情况下会被丢弃? 6 | 7 | ![](https://img-blog.csdnimg.cn/img_convert/d4df0c85e08f66f6a2aa2038af73adcc.png) 8 | 9 | 好家伙,现在面试都问那么细节了吗? 10 | 11 | 不过话说回来,这个问题跟工作上也是有关系的,因为我就在工作中碰到这么奇怪的时候,客户端向服务端发起了连接,但是连接并没有建立起来,通过抓包分析发现,服务端是收到 SYN 报文了,但是并没有回复 SYN+ACK(TCP 第二次握手),说明 SYN 报文被服务端忽略了,然后客户端就一直在超时重传 SYN 报文,直到达到最大的重传次数。 12 | 13 | 接下来,我就给出我遇到过 SYN 报文被丢弃的两种场景: 14 | 15 | - 开启 tcp_tw_recycle 参数,并且在 NAT 环境下,造成 SYN 报文被丢弃 16 | 17 | - TCP 两个队列满了(半连接队列和全连接队列),造成 SYN 报文被丢弃 18 | 19 | ## 坑爹的 tcp_tw_recycle 20 | 21 | TCP 四次挥手过程中,主动断开连接方会有一个 TIME_WAIT 的状态,这个状态会持续 2 MSL 后才会转变为 CLOSED 状态。 22 | 23 | ![](https://img-blog.csdnimg.cn/img_convert/bee0c8e8d84047e7434803fb340f9e5d.png) 24 | 25 | 在 Linux 操作系统下,TIME_WAIT 状态的持续时间是 60 秒,这意味着这 60 秒内,客户端一直会占用着这个端口。要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过如下参数设置指定范围: 26 | 27 | ```plain 28 | net.ipv4.ip_local_port_range 29 | ``` 30 | 31 | **如果客户端(发起连接方)的 TIME_WAIT 状态过多**,占满了所有端口资源,那么就无法对「目的 IP+ 目的 PORT」都一样的服务器发起连接了,但是被使用的端口,还是可以继续对另外一个服务器发起连接的。具体可以看我这篇文章:[客户端的端口可以重复使用吗?](https://xiaolincoding.com/network/3_tcp/port.html#%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%9A%84%E7%AB%AF%E5%8F%A3%E5%8F%AF%E4%BB%A5%E9%87%8D%E5%A4%8D%E4%BD%BF%E7%94%A8%E5%90%97) 32 | 33 | 因此,客户端(发起连接方)都是和「目的 IP+ 目的 PORT」都一样的服务器建立连接的话,当客户端的 TIME_WAIT 状态连接过多的话,就会受端口资源限制,如果占满了所有端口资源,那么就无法再跟「目的 IP+ 目的 PORT」都一样的服务器建立连接了。 34 | 35 | 不过,即使是在这种场景下,只要连接的是不同的服务器,端口是可以重复使用的,所以客户端还是可以向其他服务器发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源 IP、源端口、目的 IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。 36 | 37 | 但是 TIME_WAIT 状态也不是摆设作用,它的作用有两个: 38 | 39 | - 防止具有相同四元组的旧数据包被收到,也就是防止历史连接中的数据,被后面的连接接受,否则就会导致后面的连接收到一个无效的数据, 40 | - 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭; 41 | 42 | 不过,Linux 操作系统提供了两个可以系统参数来快速回收处于 TIME_WAIT 状态的连接,这两个参数都是默认关闭的: 43 | 44 | - net.ipv4.tcp_tw_reuse,如果开启该选项的话,客户端(连接发起方)在调用 connect() 函数时,**如果内核选择到的端口,已经被相同四元组的连接占用的时候,就会判断该连接是否处于 TIME_WAIT 状态,如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒,那么就会重用这个连接,然后就可以正常使用该端口了。**所以该选项只适用于连接发起方。 45 | - net.ipv4.tcp_tw_recycle,如果开启该选项的话,允许处于 TIME_WAIT 状态的连接被快速回收; 46 | 47 | 要使得这两个选项生效,有一个前提条件,就是要打开 TCP 时间戳,即 net.ipv4.tcp_timestamps=1(默认即为 1))。 48 | 49 | **tcp_tw_recycle 在使用了 NAT 的网络下是不安全的!** 50 | 51 | 对于服务器来说,如果同时开启了 recycle 和 timestamps 选项,则会开启一种称之为「per-host 的 PAWS 机制」。 52 | 53 | > 首先给大家说说什么是 PAWS 机制? 54 | 55 | tcp_timestamps 选项开启之后,PAWS 机制会自动开启,它的作用是防止 TCP 包中的序列号发生绕回。 56 | 57 | 正常来说每个 TCP 包都会有自己唯一的 SEQ,出现 TCP 数据包重传的时候会复用 SEQ 号,这样接收方能通过 SEQ 号来判断数据包的唯一性,也能在重复收到某个数据包的时候判断数据是不是重传的。**但是 TCP 这个 SEQ 号是有限的,一共 32 bit,SEQ 开始是递增,溢出之后从 0 开始再次依次递增**。 58 | 59 | 所以当 SEQ 号出现溢出后单纯通过 SEQ 号无法标识数据包的唯一性,某个数据包延迟或因重发而延迟时可能导致连接传递的数据被破坏,比如: 60 | 61 | ![](https://img-blog.csdnimg.cn/img_convert/f5fbe947240026cc2f076267cb698496.png) 62 | 63 | 上图 A 数据包出现了重传,并在 SEQ 号耗尽再次从 A 递增时,第一次发的 A 数据包延迟到达了 Server,这种情况下如果没有别的机制来保证,Server 会认为延迟到达的 A 数据包是正确的而接收,反而是将正常的第三次发的 SEQ 为 A 的数据包丢弃,造成数据传输错误。 64 | 65 | PAWS 就是为了避免这个问题而产生的,在开启 tcp_timestamps 选项情况下,一台机器发的所有 TCP 包都会带上发送时的时间戳,PAWS 要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,**如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包**。 66 | 67 | 对于上面图中的例子有了 PAWS 机制就能做到在收到 Delay 到达的 A 号数据包时,识别出它是个过期的数据包而将其丢掉。 68 | 69 | > 那什么是 per-host 的 PAWS 机制呢? 70 | 71 | 前面我提到,开启了 recycle 和 timestamps 选项,就会开启一种叫 per-host 的 PAWS 机制。**per-host 是对「对端 IP 做 PAWS 检查」**,而非对「IP + 端口」四元组做 PAWS 检查。 72 | 73 | 但是如果客户端网络环境是用了 NAT 网关,那么客户端环境的每一台机器通过 NAT 网关后,都会是相同的 IP 地址,在服务端看来,就好像只是在跟一个客户端打交道一样,无法区分出来。 74 | 75 | Per-host PAWS 机制利用 TCP option 里的 timestamp 字段的增长来判断串扰数据,而 timestamp 是根据客户端各自的 CPU tick 得出的值。 76 | 77 | 当客户端 A 通过 NAT 网关和服务器建立 TCP 连接,然后服务器主动关闭并且快速回收 TIME-WAIT 状态的连接后,**客户端 B 也通过 NAT 网关和服务器建立 TCP 连接,注意客户端 A 和 客户端 B 因为经过相同的 NAT 网关,所以是用相同的 IP 地址与服务端建立 TCP 连接,如果客户端 B 的 timestamp 比 客户端 A 的 timestamp 小,那么由于服务端的 per-host 的 PAWS 机制的作用,服务端就会丢弃客户端主机 B 发来的 SYN 包**。 78 | 79 | 因此,tcp_tw_recycle 在使用了 NAT 的网络下是存在问题的,如果它是对 TCP 四元组做 PAWS 检查,而不是对「相同的 IP 做 PAWS 检查」,那么就不会存在这个问题了。 80 | 81 | 网上很多博客都说开启 tcp_tw_recycle 参数来优化 TCP,我信你个鬼,糟老头坏的很! 82 | 83 | tcp_tw_recycle 在 Linux 4.12 版本后,直接取消了这一参数。 84 | 85 | ## accpet 队列满了 86 | 87 | 在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是: 88 | 89 | - 半连接队列,也称 SYN 队列; 90 | - 全连接队列,也称 accepet 队列; 91 | 92 | 服务端收到客户端发起的 SYN 请求后,**内核会把该连接存储到半连接队列**,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,**内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。** 93 | 94 | ![](https://img-blog.csdnimg.cn/img_convert/c9959166180b0e239bb48234ff7c2f5b.png) 95 | 96 | 97 | 98 | ### 半连接队列满了 99 | 100 | 当服务器造成 syn 攻击,就有可能导致 **TCP 半连接队列满了,这时后面来的 syn 包都会被丢弃**。 101 | 102 | 但是,**如果开启了 syncookies 功能,即使半连接队列满了,也不会丢弃 syn 包**。 103 | 104 | syncookies 是这么做的:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功,如下图所示。 105 | 106 | ![](https://img-blog.csdnimg.cn/img_convert/58e01036d1febd0103dd0ec4d5acff05.png) 107 | 108 | syncookies 参数主要有以下三个值: 109 | 110 | - 0 值,表示关闭该功能; 111 | - 1 值,表示仅当 SYN 半连接队列放不下时,再启用它; 112 | - 2 值,表示无条件开启功能; 113 | 114 | 那么在应对 SYN 攻击时,只需要设置为 1 即可: 115 | 116 | 117 | ![](https://img-blog.csdnimg.cn/img_convert/e795b4ff5be76c85814ee190b4921f25.png) 118 | 119 | 这里给出几种防御 SYN 攻击的方法: 120 | 121 | - 增大半连接队列; 122 | - 开启 tcp_syncookies 功能 123 | - 减少 SYN+ACK 重传次数 124 | 125 | *方式一:增大半连接队列* 126 | 127 | **要想增大半连接队列,我们得知不能只单纯增大 tcp_max_syn_backlog 的值,还需一同增大 somaxconn 和 backlog,也就是增大全连接队列**。否则,只单纯增大 tcp_max_syn_backlog 是无效的。 128 | 129 | 增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 内核参数: 130 | 131 | ![](https://img-blog.csdnimg.cn/img_convert/29f1fd2894162e15cbac938a2373b543.png) 132 | 133 | 增大 backlog 的方式,每个 Web 服务都不同,比如 Nginx 增大 backlog 的方法如下: 134 | 135 | ![](https://img-blog.csdnimg.cn/img_convert/a6b11fbd1fcb742cdcc87447fc23b73f.png) 136 | 137 | 最后,改变了如上这些参数后,要重启 Nginx 服务,因为半连接队列和全连接队列都是在 listen() 初始化的。 138 | 139 | *方式二:开启 tcp_syncookies 功能* 140 | 141 | 开启 tcp_syncookies 功能的方式也很简单,修改 Linux 内核参数: 142 | 143 | ![](https://img-blog.csdnimg.cn/img_convert/54b7411607978cb9ff36d88cf47eb5c4.png) 144 | 145 | *方式三:减少 SYN+ACK 重传次数* 146 | 147 | 当服务端受到 SYN 攻击时,就会有大量处于 SYN_RECV 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK,当重传超过次数达到上限后,就会断开连接。 148 | 149 | 那么针对 SYN 攻击的场景,我们可以减少 SYN+ACK 的重传次数,以加快处于 SYN_RECV 状态的 TCP 连接断开。 150 | 151 | ![](https://img-blog.csdnimg.cn/img_convert/19443a03430368b72c201113150471c5.png) 152 | 153 | ### 全连接队列满了 154 | 155 | **在服务端并发处理大量请求时,如果 TCP accpet 队列过小,或者应用程序调用 accept() 不及时,就会造成 accpet 队列满了,这时后续的连接就会被丢弃,这样就会出现服务端请求数量上不去的现象。** 156 | 157 | ![](https://img-blog.csdnimg.cn/img_convert/d1538f8d3b50da26039bc6b171a13ad1.png) 158 | 159 | 我们可以通过 ss 命令来看 accpet 队列大小,在「LISTEN 状态」时,`Recv-Q/Send-Q` 表示的含义如下: 160 | 161 | ![](https://img-blog.csdnimg.cn/img_convert/d7e8fcbb4afa583687b76064b7f1afac.png) 162 | 163 | 164 | - Recv-Q:当前 accpet 队列的大小,也就是当前已完成三次握手并等待服务端 `accept()` 的 TCP 连接个数; 165 | - Send-Q:当前 accpet 最大队列长度,上面的输出结果说明监听 8088 端口的 TCP 服务进程,accpet 队列的最大长度为 128; 166 | 167 | 如果 Recv-Q 的大小超过 Send-Q,就说明发生了 accpet 队列满的情况。 168 | 169 | 要解决这个问题,我们可以: 170 | 171 | - 调大 accpet 队列的最大长度,调大的方式是通过**调大 backlog 以及 somaxconn 参数。** 172 | - 检查系统或者代码为什么调用 accept() 不及时; 173 | 174 | 关于 SYN 队列和 accpet 队列,我之前写过一篇很详细的文章:[TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?](https://mp.weixin.qq.com/s/2qN0ulyBtO2I67NB_RnJbg) 175 | 176 | --- 177 | 178 | 好了,今天就分享到这里啦。 179 | 180 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 181 | 182 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 183 | 184 | -------------------------------------------------------------------------------- /network/3_tcp/tcp_down_and_crash.md: -------------------------------------------------------------------------------- 1 | # 4.12 TCP 连接,一端断电和进程崩溃有什么区别? 2 | 3 | 有位读者找我说,他在面试腾讯的时候,遇到了这么个问题: 4 | 5 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021061513401120.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 6 | 7 | 8 | 9 | 这个属于 **TCP 异常断开连接**的场景,这部分内容在我的「图解网络」还没有详细介绍过,这次就乘着这次机会补一补。 10 | 11 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210615134020994.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 12 | 13 | 这个问题有几个关键词: 14 | 15 | - 没有开启 keepalive; 16 | - 一直没有数据交互; 17 | - 进程崩溃; 18 | - 主机崩溃; 19 | 20 | 21 | 我们先来认识认识什么是 TCP keepalive 呢? 22 | 23 | 这东西其实就是 **TCP 的保活机制**,它的工作原理我之前的文章写过,这里就直接贴下以前的内容。 24 | 25 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210615134028909.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 26 | 27 | 28 | 29 | 如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。 30 | - 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端,对端会正常响应,这样 **TCP 保活时间会被重置**,等待下一个 TCP 保活时间的到来。 31 | - 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,**TCP 会报告该 TCP 连接已经死亡**。 32 | 33 | 34 | 所以,TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活。 35 | 36 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210615134036676.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 37 | 38 | 注意,应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 `SO_KEEPALIVE` 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。 39 | 40 | ## 主机崩溃 41 | 42 | 知道了 TCP keepalive 作用,我们再回过头看题目中的「主机崩溃」这种情况。 43 | 44 | > 在没有开启 TCP keepalive,且双方一直没有数据交互的情况下,如果客户端的「主机崩溃」了,会发生什么。 45 | 46 | 47 | 客户端主机崩溃了,服务端是**无法感知到的**,在加上服务端没有开启 TCP keepalive,又没有数据交互的情况下,**服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态**,直到服务端重启进程。 48 | 49 | 所以,我们可以得知一个点,在没有使用 TCP 保活机制且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态,并不代表另一方的连接还一定正常。 50 | 51 | ## 进程崩溃 52 | 53 | 54 | > 那题目中的「进程崩溃」的情况呢? 55 | 56 | TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。 57 | 58 | 我自己做了实验,使用 kill -9 来模拟进程崩溃的情况,发现**在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手**。 59 | 60 | 61 | 所以,即使没有开启 TCP keepalive,且双方也没有数据交互的情况下,如果其中一方的进程发生了崩溃,这个过程操作系统是可以感知的到的,于是就会发送 FIN 报文给对方,然后与对方进行 TCP 四次挥手。 62 | 63 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021061513405211.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 64 | 65 | 66 | --- 67 | 68 | ## 有数据传输的场景 69 | 70 | 以上就是对这个面试题的回答,接下来我们看看在「**有数据传输**」的场景下的一些异常情况: 71 | 72 | - 第一种,客户端主机宕机,又迅速重启,会发生什么? 73 | - 第二种,客户端主机宕机,一直没有重启,会发生什么? 74 | 75 | ### 客户端主机宕机,又迅速重启 76 | 77 | 在客户端主机宕机后,服务端向客户端发送的报文会得不到任何的响应,在一定时长后,服务端就会触发**超时重传**机制,重传未得到响应的报文。 78 | 79 | 服务端重传报文的过程中,客户端主机重启完成后,客户端的内核就会接收重传的报文,然后根据报文的信息传递给对应的进程: 80 | - 如果客户端主机上**没有**进程绑定该 TCP 报文的目标端口号,那么客户端内核就会**回复 RST 报文,重置该 TCP 连接**; 81 | - 如果客户端主机上**有**进程绑定该 TCP 报文的目标端口号,由于客户端主机重启后,之前的 TCP 连接的数据结构已经丢失了,客户端内核里协议栈会发现找不到该 TCP 连接的 socket 结构体,于是就会**回复 RST 报文,重置该 TCP 连接**。 82 | 83 | 所以,**只要有一方重启完成后,收到之前 TCP 连接的报文,都会回复 RST 报文,以断开连接**。 84 | 85 | 86 | ### 客户端主机宕机,一直没有重启 87 | 88 | 这种情况,服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开。 89 | 90 | ![](https://img-blog.csdnimg.cn/20210615134110763.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 91 | 92 | > 那 TCP 的数据报文具体重传几次呢? 93 | 94 | 在 Linux 系统中,提供一个叫 tcp_retries2 配置项,默认值是 15,如下图: 95 | 96 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210615134059647.png) 97 | 98 | 99 | 这个内核参数是控制,在 TCP 连接建立的情况下,超时重传的最大次数。 100 | 101 | 不过 tcp_retries2 设置了 15 次,并不代表 TCP 超时重传了 15 次才会通知应用程序终止该 TCP 连接,**内核会根据 tcp_retries2 设置的值,计算出一个 timeout**(*如果 tcp_retries2 =15,那么计算得到的 timeout = 924600 ms*),**如果重传间隔超过这个 timeout,则认为超过了阈值,就会停止重传,然后就会断开 TCP 连接**。 102 | 103 | 在发生超时重传的过程中,每一轮的超时时间(RTO)都是**倍数增长**的,比如如果第一轮 RTO 是 200 毫秒,那么第二轮 RTO 是 400 毫秒,第三轮 RTO 是 800 毫秒,以此类推。 104 | 105 | 而 RTO 是基于 RTT(一个包的往返时间)来计算的,如果 RTT 较大,那么计算出来的 RTO 就越大,那么经过几轮重传后,很快就达到了上面的 timeout 值了。 106 | 107 | 举个例子,如果 tcp_retries2 =15,那么计算得到的 timeout = 924600 ms,如果重传总间隔时长达到了 timeout 就会停止重传,然后就会断开 TCP 连接: 108 | 109 | - 如果 RTT 比较小,那么 RTO 初始值就约等于下限 200ms,也就是第一轮的超时时间是 200 毫秒,由于 timeout 总时长是 924600 ms,表现出来的现象刚好就是重传了 15 次,超过了 timeout 值,从而断开 TCP 连接 110 | - 如果 RTT 比较大,假设 RTO 初始值计算得到的是 1000 ms,也就是第一轮的超时时间是 1 秒,那么根本不需要重传 15 次,重传总间隔就会超过 924600 ms。 111 | 112 | 最小 RTO 和最大 RTO 是在 Linux 内核中定义好了: 113 | 114 | ```c 115 | #define TCP_RTO_MAX ((unsigned)(120*HZ)) 116 | #define TCP_RTO_MIN ((unsigned)(HZ/5)) 117 | ``` 118 | 119 | Linux 2.6+ 使用 1000 毫秒的 HZ,因此`TCP_RTO_MIN`约为 200 毫秒,`TCP_RTO_MAX`约为 120 秒。 120 | 121 | 如果`tcp_retries`设置为`15`,且 RTT 比较小,那么 RTO 初始值就约等于下限 200ms,这意味着**它需要 924.6 秒**才能将断开的 TCP 连接通知给上层(即应用程序),每一轮的 RTO 增长关系如下表格: 122 | 123 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021061513410645.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 124 | 125 | 126 | --- 127 | 128 | ## 总结 129 | 130 | 如果「**客户端进程崩溃**」,客户端的进程在发生崩溃的时候,内核会发送 FIN 报文,与服务端进行四次挥手。 131 | 132 | 但是,「**客户端主机宕机**」,那么是不会发生四次挥手的,具体后续会发生什么?还要看服务端会不会发送数据? 133 | 134 | - 如果服务端会发送数据,由于客户端已经不存在,收不到数据报文的响应报文,服务端的数据报文会超时重传,当重传总间隔时长达到一定阈值(内核会根据 tcp_retries2 设置的值计算出一个阈值)后,会断开 TCP 连接; 135 | - 如果服务端一直不会发送数据,再看服务端有没有开启 TCP keepalive 机制? 136 | - 如果有开启,服务端在一段时间没有进行数据交互时,会触发 TCP keepalive 机制,探测对方是否存在,如果探测到对方已经消亡,则会断开自身的 TCP 连接; 137 | - 如果没有开启,服务端的 TCP 连接会一直存在,并且一直保持在 ESTABLISHED 状态。 138 | 139 | 最后说句,TCP 牛逼,啥异常都考虑到了。 140 | 141 | **小林是专为大家图解的工具人,Goodbye,我们下次见!** 142 | 143 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /network/3_tcp/tcp_http_keepalive.md: -------------------------------------------------------------------------------- 1 | # 4.15 TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗? 2 | 3 | 大家好,我是小林。 4 | 5 | 之前有读者问了我这么个问题: 6 | 7 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210715090027883.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 8 | 9 | 10 | 大致问题是,**TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗?** 11 | 12 | 这是个好问题,应该有不少人都会搞混,因为这两个东西看上去太像了,很容易误以为是同一个东西。 13 | 14 | 事实上,**这两个完全是两样不同东西**,实现的层面也不同: 15 | - HTTP 的 Keep-Alive,是由**应用层(用户态)** 实现的,称为 HTTP 长连接; 16 | - TCP 的 Keepalive,是由 **TCP 层(内核态)** 实现的,称为 TCP 保活机制; 17 | 18 | 接下来,分别说说它们。 19 | 20 | ## HTTP 的 Keep-Alive 21 | 22 | HTTP 协议采用的是「请求 - 应答」的模式,也就是客户端发起了请求,服务端才会返回响应,一来一回这样子。 23 | 24 | ![请求 - 应答](https://img-blog.csdnimg.cn/img_convert/6c062074058f40ae65ed722e2d082a90.png) 25 | 26 | 27 | 由于 HTTP 是基于 TCP 传输协议实现的,客户端与服务端要进行 HTTP 通信前,需要先建立 TCP 连接,然后客户端发送 HTTP 请求,服务端收到后就返回响应,至此「请求 - 应答」的模式就完成了,随后就会释放 TCP 连接。 28 | 29 | ![一个 HTTP 请求](https://img-blog.csdnimg.cn/img_convert/9acbaebbbe07cc870858a350052d9c87.png) 30 | 31 | 32 | 如果每次请求都要经历这样的过程:建立 TCP -> 请求资源 -> 响应资源 -> 释放连接,那么此方式就是 **HTTP 短连接**,如下图: 33 | 34 | 35 | ![HTTP 短连接](https://img-blog.csdnimg.cn/img_convert/d6f6757c02e3afbf113d1048c937f8ee.png) 36 | 37 | 38 | 这样实在太累人了,一次连接只能请求一次资源。 39 | 40 | 能不能在第一个 HTTP 请求完后,先不断开 TCP 连接,让后续的 HTTP 请求继续使用此连接? 41 | 42 | 当然可以,HTTP 的 Keep-Alive 就是实现了这个功能,可以使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,这个方法称为 **HTTP 长连接**。 43 | 44 | ![HTTP 长连接](https://img-blog.csdnimg.cn/img_convert/d2b20d1cc03936332adb2a68512eb167.png) 45 | 46 | HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。 47 | 48 | 怎么才能使用 HTTP 的 Keep-Alive 功能? 49 | 50 | 在 HTTP 1.0 中默认是关闭的,如果浏览器要开启 Keep-Alive,它必须在请求的包头中添加: 51 | 52 | 53 | ```plain 54 | Connection: Keep-Alive 55 | ``` 56 | 57 | 然后当服务器收到请求,作出回应的时候,它也添加一个头在响应中: 58 | 59 | ```plain 60 | Connection: Keep-Alive 61 | ``` 62 | 63 | 这样做,连接就不会中断,而是保持连接。当客户端发送另一个请求时,它会使用同一个连接。这一直继续到客户端或服务器端提出断开连接。 64 | 65 | **从 HTTP 1.1 开始,就默认是开启了 Keep-Alive**,如果要关闭 Keep-Alive,需要在 HTTP 请求的包头里添加: 66 | 67 | ```plain 68 | Connection:close 69 | ``` 70 | 71 | 现在大多数浏览器都默认是使用 HTTP/1.1,所以 Keep-Alive 都是默认打开的。一旦客户端和服务端达成协议,那么长连接就建立好了。 72 | 73 | HTTP 长连接不仅仅减少了 TCP 连接资源的开销,而且这给 **HTTP 流水线**技术提供了可实现的基础。 74 | 75 | 所谓的 HTTP 流水线,是**客户端可以先一次性发送多个请求,而在发送过程中不需先等待服务器的回应**,可以减少整体的响应时间。 76 | 77 | 举例来说,客户端需要请求两个资源。以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。HTTP 流水线机制则允许客户端同时发出 A 请求和 B 请求。 78 | 79 | ![右边为 HTTP 流水线机制](https://img-blog.csdnimg.cn/img_convert/b3fa409edd8aa1dea830af2a69fc8a31.png) 80 | 81 | 但是**服务器还是按照顺序响应**,先回应 A 请求,完成后再回应 B 请求。 82 | 83 | 而且要等服务器响应完客户端第一批发送的请求后,客户端才能发出下一批的请求,也就说如果服务器响应的过程发生了阻塞,那么客户端就无法发出下一批的请求,此时就造成了「队头阻塞」的问题。 84 | 85 | 可能有的同学会问,如果使用了 HTTP 长连接,如果客户端完成一个 HTTP 请求后,就不再发起新的请求,此时这个 TCP 连接一直占用着不是挺浪费资源的吗? 86 | 87 | 对没错,所以为了避免资源浪费的情况,web 服务软件一般都会提供 `keepalive_timeout` 参数,用来指定 HTTP 长连接的超时时间。 88 | 89 | 比如设置了 HTTP 长连接的超时时间是 60 秒,web 服务软件就会**启动一个定时器**,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,**定时器的时间一到,就会触发回调函数来释放该连接。** 90 | 91 | ![HTTP 长连接超时](https://img-blog.csdnimg.cn/img_convert/7e995ecb2e42941342f97256707496c9.png) 92 | 93 | ## TCP 的 Keepalive 94 | 95 | TCP 的 Keepalive 这东西其实就是 **TCP 的保活机制**,它的工作原理我之前的文章写过,这里就直接贴下以前的内容。 96 | 97 | 98 | 如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。 99 | - 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端,对端会正常响应,这样 **TCP 保活时间会被重置**,等待下一个 TCP 保活时间的到来。 100 | - 如果对端主机宕机(*注意不是进程崩溃,进程崩溃后操作系统在回收进程资源的时候,会发送 FIN 报文,而主机宕机则是无法感知的,所以需要 TCP 保活机制来探测对方是不是发生了主机宕机*),或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,**TCP 会报告该 TCP 连接已经死亡**。 101 | 102 | 103 | 所以,TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活,这个工作是在内核完成的。 104 | 105 | ![TCP 保活机制](https://img-blog.csdnimg.cn/img_convert/87e138ae9f2438c8f4e2c9c46ec40b95.png) 106 | 107 | 108 | 注意,应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 `SO_KEEPALIVE` 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。 109 | 110 | 111 | ## 总结 112 | 113 | HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。 114 | 115 | 116 | TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接。 117 | 118 | 119 | --- 120 | 121 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 122 | 123 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /network/3_tcp/tcp_no_listen.md: -------------------------------------------------------------------------------- 1 | # 4.19 服务端没有 listen,客户端发起连接建立,会发生什么? 2 | 3 | 大家好,我是小林。 4 | 5 | 早上看到一个读者说面字节三面的时候,问了这个问题: 6 | 7 | ![图片](https://img-blog.csdnimg.cn/img_convert/5f5b9c96c86580e3f14978d5c10c7721.jpeg) 8 | 9 | 这位读者的角度是以为服务端没有调用 listen,客户端会 ping 不通服务器,很明显,搞错了。 10 | 11 | ping 使用的协议是 ICMP,属于网络层的事情,而面试官问的是传输层的问题。 12 | 13 | 针对这个问题,服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,然后客户端对服务端发起了 TCP 连接建立,此时那么会发生什么呢? 14 | 15 | ## 做个实验 16 | 17 | 这个问题,自己做个实验就知道了。 18 | 19 | 我用下面这个程序作为例子,绑定了 IP 地址 + 端口,而没有调用 listen。 20 | 21 | ```c 22 | /*******服务器程序 TCPServer.c ************/ 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | int main(int argc, char *argv[]) 33 | { 34 | int sockfd, ret; 35 | struct sockaddr_in server_addr; 36 | 37 | /* 服务器端创建 tcp socket 描述符 */ 38 | sockfd = socket(AF_INET, SOCK_STREAM, 0); 39 | if(sockfd < 0) 40 | { 41 | fprintf(stderr, "Socket error:%s\n\a", strerror(errno)); 42 | exit(1); 43 | } 44 | 45 | /* 服务器端填充 sockaddr 结构 */ 46 | bzero(&server_addr, sizeof(struct sockaddr_in)); 47 | server_addr.sin_family = AF_INET; 48 | server_addr.sin_addr.s_addr = htonl(INADDR_ANY); 49 | server_addr.sin_port = htons(8888); 50 | 51 | /* 绑定 ip + 端口 */ 52 | ret = bind(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)); 53 | if(ret < 0) 54 | { 55 | fprintf(stderr, "Bind error:%s\n\a", strerror(errno)); 56 | exit(1); 57 | } 58 | 59 | //没有调用 listen 60 | 61 | sleep(1000); 62 | close(sockfd); 63 | return 0; 64 | } 65 | ``` 66 | 67 | 然后,我用浏览器访问这个地址:http://121.43.173.240:8888/ 68 | 69 | ![图片](https://img-blog.csdnimg.cn/img_convert/5bdb5443db5b97ff724ab94e014af6a5.png) 70 | 71 | 报错连接服务器失败。 72 | 73 | 同时,我也用抓包工具,抓了这个过程。 74 | 75 | ![图片](https://img-blog.csdnimg.cn/img_convert/a77921ffafbbff86d07983ca0db3e6e0.png) 76 | 77 | 可以看到,客户端对服务端发起 SYN 报文后,服务端回了 RST 报文。 78 | 79 | 所以,这个问题就有了答案,**服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,然后客户端对服务端发起了连接建立,服务端会回 RST 报文。** 80 | 81 | ## 源码分析 82 | 83 | 接下来,带大家源码分析一下。 84 | 85 | Linux 内核处理收到 TCP 报文的入口函数是 tcp_v4_rcv,在收到 TCP 报文后,会调用 __inet_lookup_skb 函数找到 TCP 报文所属 socket。 86 | 87 | ```plain 88 | int tcp_v4_rcv(struct sk_buff *skb) 89 | { 90 | ... 91 | 92 | sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest); 93 | if (!sk) 94 | goto no_tcp_socket; 95 | ... 96 | } 97 | ``` 98 | 99 | __inet_lookup_skb 函数首先查找连接建立状态的 socket(__inet_lookup_established),在没有命中的情况下,才会查找监听套接口(__inet_lookup_listener)。 100 | 101 | ![图片](https://img-blog.csdnimg.cn/img_convert/88416aa95d255495e07fb3a002b2167b.png) 102 | 103 | 查找监听套接口(__inet_lookup_listener)这个函数的实现是,根据目的地址和目的端口算出一个哈希值,然后在哈希表找到对应监听该端口的 socket。 104 | 105 | 本次的案例中,服务端是没有调用 listen 函数的,所以自然也是找不到监听该端口的 socket。 106 | 107 | 所以,__inet_lookup_skb 函数最终找不到对应的 socket,于是跳转到 no_tcp_socket。 108 | 109 | ![图片](https://img-blog.csdnimg.cn/img_convert/54ee363e149ee3dfba30efb1a542ef5c.png) 110 | 111 | 在这个错误处理中,只要收到的报文(skb)的「校验和」没问题的话,内核就会调用 tcp_v4_send_reset 发送 RST 中止这个连接。 112 | 113 | 至此,整个源码流程就解析完。 114 | 115 | 其实很多网络的问题,大家都可以自己做实验来找到答案的。 116 | 117 | ![图片](https://img-blog.csdnimg.cn/img_convert/8d04584bf7fa40f02229d611a569f370.jpeg) 118 | 119 | ## 没有 listen,能建立 TCP 连接吗? 120 | 121 | 标题的问题在前面已经解答,**现在我们看另外一个相似的问题**。 122 | 123 | 之前看群消息,看到有读者面试腾讯的时候,被问到这么一个问题。 124 | 125 | > 不使用 listen,可以建立 TCP 连接吗? 126 | 127 | 答案,**是可以的,客户端是可以自己连自己的形成连接(TCP 自连接),也可以两个客户端同时向对方发出请求建立连接(TCP 同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有 listen,就能建立连接**。 128 | 129 | > 那没有 listen,为什么还能建立连接? 130 | 131 | 我们知道执行 listen 方法时,会创建半连接队列和全连接队列。 132 | 133 | 三次握手的过程中会在这两个队列中暂存连接信息。 134 | 135 | 所以形成连接,前提是你得有个地方存放着,方便握手的时候能根据 IP + 端口等信息找到对应的 socket。 136 | 137 | > 那么客户端会有半连接队列吗? 138 | 139 | 显然没有,因为客户端没有执行 listen,因为半连接队列和全连接队列都是在执行 listen 方法时,内核自动创建的。 140 | 141 | 但内核还有个全局 hash 表,可以用于存放 sock 连接的信息。 142 | 143 | 这个全局 hash 表其实还细分为 ehash,bhash 和 listen_hash 等,但因为过于细节,大家理解成有一个全局 hash 就够了, 144 | 145 | **在 TCP 自连接的情况中,客户端在 connect 方法时,最后会将自己的连接信息放入到这个全局 hash 表中,然后将信息发出,消息在经过回环地址重新回到 TCP 传输层的时候,就会根据 IP + 端口信息,再一次从这个全局 hash 中取出信息。于是握手包一来一回,最后成功建立连接**。 146 | 147 | TCP 同时打开的情况也类似,只不过从一个客户端变成了两个客户端而已。 148 | 149 | > 做个实验 150 | 151 | 客户端自连接的代码,TCP socket 可以 connect 它本身 bind 的地址和端口: 152 | 153 | 154 | ```c 155 | #include 156 | #include 157 | #include 158 | #include 159 | #include 160 | #include 161 | #include 162 | #include 163 | 164 | #define LOCAL_IP_ADDR (0x7F000001) // IP 127.0.0.1 165 | #define LOCAL_TCP_PORT (34567) // 端口 166 | 167 | int main(void) 168 | { 169 | struct sockaddr_in local, peer; 170 | int ret; 171 | char buf[128]; 172 | int sock = socket(AF_INET, SOCK_STREAM, 0); 173 | 174 | memset(&local, 0, sizeof(local)); 175 | memset(&peer, 0, sizeof(peer)); 176 | 177 | local.sin_family = AF_INET; 178 | local.sin_port = htons(LOCAL_TCP_PORT); 179 | local.sin_addr.s_addr = htonl(LOCAL_IP_ADDR); 180 | 181 | peer = local; 182 | 183 | int flag = 1; 184 | ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); 185 | if (ret == -1) { 186 | printf("Fail to setsocket SO_REUSEADDR: %s\n", strerror(errno)); 187 | exit(1); 188 | } 189 | 190 | ret = bind(sock, (const struct sockaddr *)&local, sizeof(local)); 191 | if (ret) { 192 | printf("Fail to bind: %s\n", strerror(errno)); 193 | exit(1); 194 | } 195 | 196 | ret = connect(sock, (const struct sockaddr *)&peer, sizeof(peer)); 197 | if (ret) { 198 | printf("Fail to connect myself: %s\n", strerror(errno)); 199 | exit(1); 200 | } 201 | 202 | printf("Connect to myself successfully\n"); 203 | 204 | //发送数据 205 | strcpy(buf, "Hello, myself~"); 206 | send(sock, buf, strlen(buf), 0); 207 | 208 | memset(buf, 0, sizeof(buf)); 209 | 210 | //接收数据 211 | recv(sock, buf, sizeof(buf), 0); 212 | printf("Recv the msg: %s\n", buf); 213 | 214 | sleep(1000); 215 | close(sock); 216 | return 0; 217 | } 218 | ``` 219 | 220 | 编译运行: 221 | 222 | ![](https://img-blog.csdnimg.cn/9db974179b9e4a279f7edb0649752c27.png) 223 | 224 | 225 | 通过 netstat 命令命令客户端自连接的 TCP 连接: 226 | 227 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/e2b116e843c14e468eadf9d30e1b877c.png) 228 | 229 | 从截图中,可以看到 TCP socket 成功的“连接”了自己,并发送和接收了数据包,netstat 的输出更证明了 TCP 的两端地址和端口是完全相同的。 230 | 231 | --- 232 | 233 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 234 | 235 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /network/3_tcp/tcp_problem.md: -------------------------------------------------------------------------------- 1 | # 4.16 TCP 协议有什么缺陷? 2 | 3 | 大家好,我是小林。 4 | 5 | 写的多了后,忽然思考一个问题,TCP 通过序列号、确认应答、超时重传、流量控制、拥塞控制等方式实现了可靠传输,看起来它很完美,事实真的是这样吗?TCP 就没什么缺陷吗? 6 | 7 | 所以,今天就跟大家聊聊,TCP 协议有哪些缺陷?主要有四个方面: 8 | 9 | - 升级 TCP 的工作很困难; 10 | - TCP 建立连接的延迟; 11 | - TCP 存在队头阻塞问题; 12 | - 网络迁移需要重新建立 TCP 连接; 13 | 14 | 接下来,针对这四个方面详细说一下。 15 | 16 | ## 升级 TCP 的工作很困难 17 | 18 | TCP 协议是诞生在 1973 年,至今 TCP 协议依然还在实现更多的新特性。 19 | 20 | 但是 TCP 协议是在内核中实现的,应用程序只能使用不能修改,如果要想升级 TCP 协议,那么只能升级内核。 21 | 22 | 而升级内核这个工作是很麻烦的事情,麻烦的事情不是说升级内核这个操作很麻烦,而是由于内核升级涉及到底层软件和运行库的更新,我们的服务程序就需要回归测试是否兼容新的内核版本,所以服务器的内核升级也比较保守和缓慢。 23 | 24 | 很多 TCP 协议的新特性,都是需要客户端和服务端同时支持才能生效的,比如 TCP Fast Open 这个特性,虽然在 2013 年就被提出了,但是 Windows 很多系统版本依然不支持它,这是因为 PC 端的系统升级滞后很严重,W indows Xp 现在还有大量用户在使用,尽管它已经存在快 20 年。 25 | 26 | 所以,即使 TCP 有比较好的特性更新,也很难快速推广,用户往往要几年或者十年才能体验到。 27 | 28 | ## TCP 建立连接的延迟 29 | 30 | 基于 TCP 实现的应用协议,都是需要先建立三次握手才能进行数据传输,比如 HTTP 1.0/1.1、HTTP/2、HTTPS。 31 | 32 | 现在大多数网站都是使用 HTTPS 的,这意味着在 TCP 三次握手之后,还需要经过 TLS 四次握手后,才能进行 HTTP 数据的传输,这在一定程序上增加了数据传输的延迟。 33 | 34 | TCP 三次握手和 TLS 握手延迟,如图: 35 | 36 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E7%BD%91%E7%BB%9C/http3/TCP%2BTLS.gif) 37 | 38 | TCP 三次握手的延迟被 TCP Fast Open(快速打开)这个特性解决了,这个特性可以在「第二次建立连接」时减少 TCP 连接建立的时延。 39 | 40 | ![常规 HTTP 请求 与 Fast Open HTTP 请求](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-Wireshark/45.jpg) 41 | 42 | 过程如下: 43 | 44 | - 在第一次建立连接的时候,服务端在第二次握手产生一个 `Cookie` (已加密)并通过 SYN、ACK 包一起发给客户端,于是客户端就会缓存这个 `Cookie`,所以第一次发起 HTTP Get 请求的时候,还是需要 2 个 RTT 的时延; 45 | - 在下次请求的时候,客户端在 SYN 包带上 `Cookie` 发给服务端,就提前可以跳过三次握手的过程,因为 `Cookie` 中维护了一些信息,服务端可以从 `Cookie` 获取 TCP 相关的信息,这时发起的 HTTP GET 请求就只需要 1 个 RTT 的时延; 46 | 47 | TCP Fast Open 这个特性是不错,但是它需要服务端和客户端的操作系统同时支持才能体验到,而 TCP Fast Open 是在 2013 年提出的,所以市面上依然有很多老式的操作系统不支持,而升级操作系统是很麻烦的事情,因此 TCP Fast Open 很难被普及开来。 48 | 49 | 还有一点,针对 HTTPS 来说,TLS 是在应用层实现的握手,而 TCP 是在内核实现的握手,这两个握手过程是无法结合在一起的,总是得先完成 TCP 握手,才能进行 TLS 握手。 50 | 51 | 也正是 TCP 是在内核实现的,所以 TLS 是无法对 TCP 头部加密的,这意味着 TCP 的序列号都是明文传输,所以就存安全的问题。 52 | 53 | 一个典型的例子就是攻击者伪造一个的 RST 报文强制关闭一条 TCP 连接,而攻击成功的关键则是 TCP 字段里的序列号位于接收方的滑动窗口内,该报文就是合法的。 54 | 55 | 为此 TCP 也不得不进行三次握手来同步各自的序列号,而且初始化序列号时是采用随机的方式(不完全随机,而是随着时间流逝而线性增长,到了 2^32 尽头再回滚)来提升攻击者猜测序列号的难度,以增加安全性。 56 | 57 | 但是这种方式只能避免攻击者预测出合法的 RST 报文,而无法避免攻击者截获客户端的报文,然后中途伪造出合法 RST 报文的攻击的方式。 58 | 59 | ![](https://gw.alipayobjects.com/mdn/rms_1c90e8/afts/img/A*po6LQIBU7zIAAAAAAAAAAAAAARQnAQ) 60 | 61 | 大胆想一下,如果 TCP 的序列号也能被加密,或许真的不需要三次握手了,客户端和服务端的初始序列号都从 0 开始,也就不用做同步序列号的工作了,但是要实现这个要改造整个协议栈,太过于麻烦,即使实现出来了,很多老的网络设备未必能兼容。 62 | 63 | ## TCP 存在队头阻塞问题 64 | 65 | TCP 是字节流协议,**TCP 层必须保证收到的字节数据是完整且有序的**,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据。如下图: 66 | 67 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E7%BD%91%E7%BB%9C/http3/tcp%E9%98%9F%E5%A4%B4%E9%98%BB%E5%A1%9E.gif) 68 | 69 | 图中发送方发送了很多个 packet,每个 packet 都有自己的序号,你可以认为是 TCP 的序列号,其中 `packet #3` 在网络中丢失了,即使 `packet #4-6` 被接收方收到后,由于内核中的 TCP 数据不是连续的,于是接收方的应用层就无法从内核中读取到,只有等到 `packet #3` 重传后,接收方的应用层才可以从内核中读取到数据。 70 | 71 | 这就是 TCP 队头阻塞问题,但这也不能怪 TCP,因为只有这样做才能保证数据的有序性。 72 | 73 | HTTP/2 多个请求是跑在一个 TCP 连接中的,那么当 TCP 丢包时,整个 TCP 都要等待重传,那么就会阻塞该 TCP 连接中的所有请求,所以 HTTP/2 队头阻塞问题就是因为 TCP 协议导致的。 74 | 75 | ![](https://pic2.zhimg.com/80/v2-2dd2a9fb8693489b9a0b24771c8a40a1_1440w.jpg) 76 | 77 | 78 | 79 | ## 网络迁移需要重新建立 TCP 连接 80 | 81 | 基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接。 82 | 83 | ![TCP 四元组](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L2doL3hpYW9saW5jb2Rlci9JbWFnZUhvc3QyLyVFOCVBRSVBMSVFNyVBRSU5NyVFNiU5QyVCQSVFNyVCRCU5MSVFNyVCQiU5Qy9UQ1AtJUU0JUI4JTg5JUU2JUFDJUExJUU2JThGJUExJUU2JTg5JThCJUU1JTkyJThDJUU1JTlCJTlCJUU2JUFDJUExJUU2JThDJUE1JUU2JTg5JThCLzEwLmpwZw?x-oss-process=image/format,png) 84 | 85 | 那么**当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立 TCP 连接**。 86 | 87 | 而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。 88 | 89 | ## 结尾 90 | 91 | 我记得之前在群里看到,有位读者字节一面的时候被问到:「**如何基于 UDP 协议实现可靠传输?**」 92 | 93 | 很多同学第一反应就会说把 TCP 可靠传输的特性(序列号、确认应答、超时重传、流量控制、拥塞控制)在应用层实现一遍。 94 | 95 | 实现的思路确实这样没错,但是有没有想过,**既然 TCP 天然支持可靠传输,为什么还需要基于 UDP 实现可靠传输呢?这不是重复造轮子吗?** 96 | 97 | 所以,我们要先弄清楚 TCP 协议有哪些痛点?而这些痛点是否可以在基于 UDP 协议实现的可靠传输协议中得到改进? 98 | 99 | 现在市面上已经有基于 UDP 协议实现的可靠传输协议的成熟方案了,那就是 QUIC 协议,**QUIC 协议把我本文说的 TCP 的缺点都给解决了**,而且已经应用在了 HTTP/3。 100 | 101 | ![](https://miro.medium.com/max/1400/1*uk5OZPL7gtUwqRLwaoGyFw.png) 102 | 103 | --- 104 | 105 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 106 | 107 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 108 | 109 | -------------------------------------------------------------------------------- /network/3_tcp/tcp_stream.md: -------------------------------------------------------------------------------- 1 | # 4.6 如何理解是 TCP 面向字节流协议? 2 | 3 | 有个读者问我,这么个问题: 4 | 5 | > TCP 是面向字节流的协议,UDP 是面向报文的协议?这里的「面向字节流」和「面向报文」该如何理解。 6 | 7 | 8 | ------ 9 | 10 | ## 如何理解字节流? 11 | 12 | 之所以会说 TCP 是面向字节流的协议,UDP 是面向报文的协议,是因为操作系统对 TCP 和 UDP 协议的**发送方的机制不同**,也就是问题原因在发送方。 13 | 14 | > 先来说说为什么 UDP 是面向报文的协议? 15 | 16 | 当用户消息通过 UDP 协议传输时,**操作系统不会对消息进行拆分**,在组装好 UDP 头部后就交给网络层来处理,所以发出去的 UDP 报文中的数据部分就是完整的用户消息,也就是**每个 UDP 报文就是一个用户消息的边界**,这样接收方在接收到 UDP 报文后,读一个 UDP 报文就能读取到完整的用户消息。 17 | 18 | 你可能会问,如果收到了两个 UDP 报文,操作系统是怎么区分开的? 19 | 20 | 操作系统在收到 UDP 报文后,会将其插入到队列里,**队列里的每一个元素就是一个 UDP 报文**,这样当用户调用 recvfrom() 系统调用读数据的时候,就会从队列里取出一个数据,然后从内核里拷贝给用户缓冲区。 21 | 22 | ![图片](https://img-blog.csdnimg.cn/img_convert/a9116c5b375d356048df033dcb53582e.png) 23 | 24 | 25 | 26 | > 再来说说为什么 TCP 是面向字节流的协议? 27 | 28 | 当用户消息通过 TCP 协议传输时,**消息可能会被操作系统分组成多个的 TCP 报文**,也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。 29 | 30 | 这时,接收方的程序如果不知道发送方发送的消息的长度,也就是不知道消息的边界时,是无法读出一个有效的用户消息的,因为用户消息被拆分成多个 TCP 报文后,并不能像 UDP 那样,一个 UDP 报文就能代表一个完整的用户消息。 31 | 32 | 举个实际的例子来说明。 33 | 34 | 发送方准备发送「Hi.」和「I am Xiaolin」这两个消息。 35 | 36 | 在发送端,当我们调用 send 函数完成数据“发送”以后,数据并没有被真正从网络上发送出去,只是从应用程序拷贝到了操作系统内核协议栈中。 37 | 38 | 至于什么时候真正被发送,**取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件**。也就是说,我们不能认为每次 send 调用发送的数据,都会作为一个整体完整地消息被发送出去。 39 | 40 | 如果我们考虑实际网络传输过程中的各种影响,假设发送端陆续调用 send 函数先后发送「Hi.」和「I am Xiaolin」报文,那么实际的发送很有可能是这几种情况。 41 | 42 | 第一种情况,这两个消息被分到同一个 TCP 报文,像这样: 43 | 44 | ![图片](https://img-blog.csdnimg.cn/img_convert/02dce678f870c8c70482b6e37dbb5574.png) 45 | 46 | 第二种情况,「I am Xiaolin」的部分随「Hi」在一个 TCP 报文中发送出去,像这样: 47 | 48 | ![图片](https://img-blog.csdnimg.cn/img_convert/f58b70cde860188b8f95a433e2f5293b.png) 49 | 50 | 第三种情况,「Hi.」的一部分随 TCP 报文被发送出去,另一部分和「I am Xiaolin」一起随另一个 TCP 报文发送出去,像这样。 51 | 52 | ![图片](https://img-blog.csdnimg.cn/img_convert/68080e783d7acc842fa254e4f9ec5630.png) 53 | 54 | 类似的情况还能举例很多种,这里主要是想说明,我们不知道「Hi.」和「I am Xiaolin」这两个用户消息是如何进行 TCP 分组传输的。 55 | 56 | 因此,**我们不能认为一个用户消息对应一个 TCP 报文,正因为这样,所以 TCP 是面向字节流的协议**。 57 | 58 | 当两个消息的某个部分内容被分到同一个 TCP 报文时,就是我们常说的 TCP 粘包问题,这时接收方不知道消息的边界的话,是无法读出有效的消息。 59 | 60 | 要解决这个问题,要交给**应用程序**。 61 | 62 | ## 如何解决粘包? 63 | 64 | 粘包的问题出现是因为不知道一个用户消息的边界在哪,如果知道了边界在哪,接收方就可以通过边界来划分出有效的用户消息。 65 | 66 | 一般有三种方式分包的方式: 67 | 68 | - 固定长度的消息; 69 | - 特殊字符作为边界; 70 | - 自定义消息结构。 71 | 72 | #### 固定长度的消息 73 | 74 | 这种是最简单方法,即每个用户消息都是固定长度的,比如规定一个消息的长度是 64 个字节,当接收方接满 64 个字节,就认为这个内容是一个完整且有效的消息。 75 | 76 | 但是这种方式灵活性不高,实际中很少用。 77 | 78 | ### 特殊字符作为边界 79 | 80 | 我们可以在两个用户消息之间插入一个特殊的字符串,这样接收方在接收数据时,读到了这个特殊字符,就把认为已经读完一个完整的消息。 81 | 82 | HTTP 是一个非常好的例子。 83 | 84 | ![图片](https://img-blog.csdnimg.cn/img_convert/a49a6bb8cd38ae1738d9c00aec68b444.png) 85 | 86 | HTTP 通过设置回车符、换行符作为 HTTP 报文协议的边界。 87 | 88 | 有一点要注意,这个作为边界点的特殊字符,如果刚好消息内容里有这个特殊字符,我们要对这个字符转义,避免被接收方当作消息的边界点而解析到无效的数据。 89 | 90 | ### 自定义消息结构 91 | 92 | 我们可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。 93 | 94 | 比如这个消息结构体,首先 4 个字节大小的变量来表示数据长度,真正的数据则在后面。 95 | 96 | ```c 97 | struct { 98 | u_int32_t message_length; 99 | char message_data[]; 100 | } message; 101 | ``` 102 | 103 | 当接收方接收到包头的大小(比如 4 个字节)后,就解析包头的内容,于是就可以知道数据的长度,然后接下来就继续读取数据,直到读满数据的长度,就可以组装成一个完整的用户消息来处理了。 104 | -------------------------------------------------------------------------------- /network/3_tcp/tcp_tls.md: -------------------------------------------------------------------------------- 1 | # 4.14 HTTPS 中 TLS 和 TCP 能同时握手吗? 2 | 3 | 大家好,我是小林。 4 | 5 | 有位读者在面试的时候,碰到这么个问题: 6 | 7 | ![图片](https://img-blog.csdnimg.cn/img_convert/4d07f1ab714bb4b3efefbf5655b2f81e.png) 8 | 9 | 面试官跟他说 **HTTPS 中的 TLS 握手过程可以同时进行三次握手**,然后读者之前看我的文章是说「*先进行 TCP 三次握手,再进行 TLS 四次握手*」,他跟面试官说了这个,面试官说他不对,他就感到很困惑。 10 | 11 | 我们先不管面试官说的那句「*HTTPS 中的 TLS 握手过程可以同时进行三次握手*」对不对。 12 | 13 | 但是面试官说「*HTTPS 建立连接的过程,先进行 TCP 三次握手,再进行 TLS 四次握手*」是错的,**这很明显面试官的水平有问题,这种公司不去也罢!** 14 | 15 | 如果是我面试遇到这样的面试官,我直接当场给他抓 HTTPS 建立过程的网络包,然后给他看,啪啪啪啪啪的打他脸。 16 | 17 | 比如,下面这个 TLSv1.2 的 基于 RSA 算法的四次握手过程: 18 | 19 | ![图片](https://img-blog.csdnimg.cn/img_convert/4e4f0d13effbeaf963992148b022ef3f.png) 20 | 21 | 难道不是先三次握手,再进行 TLS 四次握手吗?面试官你脸疼吗? 22 | 23 | 不过 TLS 握手过程的次数还得看版本。 24 | 25 | TLSv1.2 握手过程基本都是需要四次,也就是需要经过 2-RTT 才能完成握手,然后才能发送请求,而 TLSv1.3 只需要 1-RTT 就能完成 TLS 握手,如下图。 26 | 27 | ![图片](https://img-blog.csdnimg.cn/img_convert/0877fe78380bf34ad3b28768e59fb53a.png) 28 | 29 | **一般情况下,不管 TLS 握手次数如何,都得先经过 TCP 三次握手后才能进行**,因为 HTTPS 都是基于 TCP 传输协议实现的,得先建立完可靠的 TCP 连接才能做 TLS 握手的事情。 30 | 31 | > 那面试官说的这句「HTTPS 中的 TLS 握手过程可以同时进行三次握手」对不对呢? 32 | 33 | 这个场景是可能发生的,但是需要在特定的条件下才可能发生,**如果没有说任何前提条件,说这句话就是在耍流氓。** 34 | 35 | 那到底什么条件下,这个场景才能发生呢?需要下面这两个条件同时满足才可以: 36 | 37 | - **客户端和服务端都开启了 TCP Fast Open 功能,且 TLS 版本是 1.3;** 38 | - **客户端和服务端已经完成过一次通信。** 39 | 40 | 那具体怎么做到的呢?我们先了解些 TCP Fast Open 功能和 TLSv1.3 的特性。 41 | 42 | ## TCP Fast Open 43 | 44 | > 我们先来了解下什么是 TCP Fast Open? 45 | 46 | 常规的情况下,如果要使用 TCP 传输协议进行通信,则客户端和服务端通信之前,先要经过 TCP 三次握手后,建立完可靠的 TCP 连接后,客户端才能将数据发送给服务端。 47 | 48 | 其中,TCP 的第一次和第二次握手是不能够携带数据的,而 TCP 的第三次握手是可以携带数据的,因为这时候客户端的 TCP 连接状态已经是 ESTABLISHED,表明客户端这一方已经完成了 TCP 连接建立。 49 | 50 | ![图片](https://img-blog.csdnimg.cn/img_convert/35bc3541c237686aa36e0a88f80592d4.png) 51 | 52 | 就算客户端携带数据的第三次握手在网络中丢失了,客户端在一定时间内没有收到服务端对该数据的应答报文,就会触发超时重传机制,然后客户端重传该携带数据的第三次握手的报文,直到重传次数达到系统的阈值,客户端就会销毁该 TCP 连接。 53 | 54 | 说完常规的 TCP 连接后,我们再来看看 TCP Fast Open。 55 | 56 | TCP Fast Open 是为了绕过 TCP 三次握手发送数据,在 Linux 3.7 内核版本之后,提供了 TCP Fast Open 功能,这个功能可以减少 TCP 连接建立的时延。 57 | 58 | 要使用 TCP Fast Open 功能,客户端和服务端都要同时支持才会生效。 59 | 60 | 不过,开启了 TCP Fast Open 功能,**想要绕过 TCP 三次握手发送数据,得建立第二次以后的通信过程。** 61 | 62 | 在客户端首次建立连接时的过程,如下图: 63 | 64 | ![图片](https://img-blog.csdnimg.cn/img_convert/7cb0bd3cde30493fec9562cbdb549f83.png) 65 | 66 | 具体介绍: 67 | 68 | - 客户端发送 SYN 报文,该报文包含 Fast Open 选项,且该选项的 Cookie 为空,这表明客户端请求 Fast Open Cookie; 69 | - 支持 TCP Fast Open 的服务器生成 Cookie,并将其置于 SYN-ACK 报文中的 Fast Open 选项以发回客户端; 70 | - 客户端收到 SYN-ACK 后,本地缓存 Fast Open 选项中的 Cookie。 71 | 72 | 所以,第一次客户端和服务端通信的时候,还是需要正常的三次握手流程。随后,客户端就有了 Cookie 这个东西,它可以用来向服务器 TCP 证明先前与客户端 IP 地址的三向握手已成功完成。 73 | 74 | 对于客户端与服务端的后续通信,客户端可以在第一次握手的时候携带应用数据,从而达到绕过三次握手发送数据的效果,整个过程如下图: 75 | 76 | ![图片](https://img-blog.csdnimg.cn/img_convert/fc452688b9351e0cabf60212dde3f21e.png) 77 | 78 | 我详细介绍下这个过程: 79 | 80 | - 客户端发送 SYN 报文,该报文可以携带「应用数据」以及此前记录的 Cookie; 81 | - 支持 TCP Fast Open 的服务器会对收到 Cookie 进行校验:如果 Cookie 有效,服务器将在 SYN-ACK 报文中对 SYN 和「数据」进行确认,服务器随后将「应用数据」递送给对应的应用程序;如果 Cookie 无效,服务器将丢弃 SYN 报文中包含的「应用数据」,且其随后发出的 SYN-ACK 报文将只确认 SYN 的对应序列号; 82 | - **如果服务器接受了 SYN 报文中的「应用数据」,服务器可在握手完成之前发送「响应数据」,这就减少了握手带来的 1 个 RTT 的时间消耗**; 83 | - 客户端将发送 ACK 确认服务器发回的 SYN 以及「应用数据」,但如果客户端在初始的 SYN 报文中发送的「应用数据」没有被确认,则客户端将重新发送「应用数据」; 84 | - 此后的 TCP 连接的数据传输过程和非 TCP Fast Open 的正常情况一致。 85 | 86 | 所以,如果客户端和服务端同时支持 TCP Fast Open 功能,那么在完成首次通信过程后,后续客户端与服务端 的通信则可以绕过三次握手发送数据,这就减少了握手带来的 1 个 RTT 的时间消耗。 87 | 88 | ## TLSv1.3 89 | 90 | > 说完 TCP Fast Open,再来看看 TLSv1.3。 91 | 92 | 在最开始的时候,我也提到 TLSv1.3 握手过程只需 1-RTT 的时间,它到整个握手过程,如下图: 93 | 94 | ![图片](https://img-blog.csdnimg.cn/img_convert/1fd5ba4000f82613fdd70cab6da4b9cb.png) 95 | 96 | TCP 连接的第三次握手是可以携带数据的,如果客户端在第三次握手发送了 TLSv1.3 第一次握手数据,是不是就表示「*HTTPS 中的 TLS 握手过程可以同时进行三次握手*」?。 97 | 98 | 不是的,因为服务端只有在收到客户端的 TCP 的第三次握手后,才能和客户端进行后续 TLSv1.3 握手。 99 | 100 | TLSv1.3 还有个更厉害到地方在于**会话恢复**机制,在**重连 TLvS1.3 只需要 0-RTT**,用“pre_shared_key”和“early_data”扩展,在 TCP 连接后立即就建立安全连接发送加密消息,过程如下图: 101 | 102 | ![图片](https://img-blog.csdnimg.cn/img_convert/59539201f006d7dc0a06333617e5ea85.png) 103 | 104 | ## TCP Fast Open + TLSv1.3 105 | 106 | 在前面我们知道,客户端和服务端同时支持 TCP Fast Open 功能的情况下,**在第二次以后到通信过程中,客户端可以绕过三次握手直接发送数据,而且服务端也不需要等收到第三次握手后才发送数据。** 107 | 108 | 如果 HTTPS 的 TLS 版本是 1.3,那么 TLS 过程只需要 1-RTT。 109 | 110 | **因此如果「TCP Fast Open + TLSv1.3」情况下,在第二次以后的通信过程中,TLS 和 TCP 的握手过程是可以同时进行的。** 111 | 112 | **如果基于 TCP Fast Open 场景下的 TLSv1.3 0-RTT 会话恢复过程,不仅 TLS 和 TCP 的握手过程是可以同时进行的,而且 HTTP 请求也可以在这期间内一同完成。** 113 | 114 | ## 总结 115 | 116 | 最后做个总结。 117 | 118 | 「HTTPS 是先进行 TCP 三次握手,再进行 TLSv1.2 四次握手」,这句话一点问题都没有,怀疑这句话是错的人,才有问题。 119 | 120 | 「HTTPS 中的 TLS 握手过程可以同时进行三次握手」,这个场景是可能存在到,但是在没有说任何前提条件,而说这句话就等于耍流氓。需要下面这两个条件同时满足才可以: 121 | 122 | - **客户端和服务端都开启了 TCP Fast Open 功能,且 TLS 版本是 1.3;** 123 | - **客户端和服务端已经完成过一次通信;** 124 | 125 | 怎么样,那位“面试官”学废了吗? 126 | 127 | --- 128 | 129 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 130 | 131 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /network/3_tcp/tcp_unplug_the_network_cable.md: -------------------------------------------------------------------------------- 1 | # 4.13 拔掉网线后,原本的 TCP 连接还存在吗? 2 | 3 | 大家好,我是小林。 4 | 5 | 今天,聊一个有趣的问题:**拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?** 6 | 7 | 可能有的同学会说,网线都被拔掉了,那说明物理层被断开了,那在上层的传输层理应也会断开,所以原本的 TCP 连接就不会存在的了。就好像,我们拨打有线电话的时候,如果某一方的电话线被拔了,那么本次通话就彻底断了。 8 | 9 | 真的是这样吗? 10 | 11 | 上面这个逻辑就有问题。问题在于,错误的认为拔掉网线这个动作会影响传输层,事实上并不会影响。 12 | 13 | 实际上,TCP 连接在 Linux 内核中是一个名为 `struct socket` 的结构体,该结构体的内容包含 TCP 连接的状态等信息。当拔掉网线的时候,操作系统并不会变更该结构体的任何内容,所以 TCP 连接的状态也不会发生改变。 14 | 15 | 我在我的电脑上做了个小实验,我用 ssh 终端连接了我的云服务器,然后我通过断开 wifi 的方式来模拟拔掉网线的场景,此时查看 TCP 连接的状态没有发生变化,还是处于 ESTABLISHED 状态。 16 | 17 | ![图片](https://img-blog.csdnimg.cn/img_convert/fff358407ee92aeea1e17386191a5d18.png) 18 | 19 | 通过上面这个实验结果,我们知道了,拔掉网线这个动作并不会影响 TCP 连接的状态。 20 | 21 | 接下来,要看拔掉网线后,双方做了什么动作。 22 | 23 | 所以,针对这个问题,要分场景来讨论: 24 | 25 | - 拔掉网线后,有数据传输; 26 | - 拔掉网线后,没有数据传输; 27 | 28 | ## 拔掉网线后,有数据传输 29 | 30 | 在客户端拔掉网线后,服务端向客户端发送的数据报文会得不到任何的响应,在等待一定时长后,服务端就会触发**超时重传**机制,重传未得到响应的数据报文。 31 | 32 | **如果在服务端重传报文的过程中,客户端刚好把网线插回去了**,由于拔掉网线并不会改变客户端的 TCP 连接状态,并且还是处于 ESTABLISHED 状态,所以这时客户端是可以正常接收服务端发来的数据报文的,然后客户端就会回 ACK 响应报文。 33 | 34 | 此时,客户端和服务端的 TCP 连接依然存在的,就感觉什么事情都没有发生。 35 | 36 | 但是,**如果在服务端重传报文的过程中,客户端一直没有将网线插回去**,服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开。 37 | 38 | 而等客户端插回网线后,如果客户端向服务端发送了数据,由于服务端已经没有与客户端相同四元祖的 TCP 连接了,因此服务端内核就会回复 RST 报文,客户端收到后就会释放该 TCP 连接。 39 | 40 | 此时,客户端和服务端的 TCP 连接都已经断开了。 41 | 42 | > 那 TCP 的数据报文具体重传几次呢? 43 | 44 | 在 Linux 系统中,提供了一个叫 tcp_retries2 配置项,默认值是 15,如下图: 45 | 46 | ![图片](https://img-blog.csdnimg.cn/img_convert/f92c00c7e9cd01e89326e943232e5f04.png) 47 | 48 | 这个内核参数是控制,在 TCP 连接建立的情况下,超时重传的最大次数。 49 | 50 | 不过 tcp_retries2 设置了 15 次,并不代表 TCP 超时重传了 15 次才会通知应用程序终止该 TCP 连接,**内核会根据 tcp_retries2 设置的值,计算出一个 timeout**(*如果 tcp_retries2 =15,那么计算得到的 timeout = 924600 ms*),**如果重传间隔超过这个 timeout,则认为超过了阈值,就会停止重传,然后就会断开 TCP 连接**。 51 | 52 | 在发生超时重传的过程中,每一轮的超时时间(RTO)都是**倍数增长**的,比如如果第一轮 RTO 是 200 毫秒,那么第二轮 RTO 是 400 毫秒,第三轮 RTO 是 800 毫秒,以此类推。 53 | 54 | 而 RTO 是基于 RTT(一个包的往返时间)来计算的,如果 RTT 较大,那么计算出来的 RTO 就越大,那么经过几轮重传后,很快就达到了上面的 timeout 值了。 55 | 56 | 举个例子,如果 tcp_retries2 =15,那么计算得到的 timeout = 924600 ms,如果重传总间隔时长达到了 timeout 就会停止重传,然后就会断开 TCP 连接: 57 | 58 | - 如果 RTT 比较小,那么 RTO 初始值就约等于下限 200ms,也就是第一轮的超时时间是 200 毫秒,由于 timeout 总时长是 924600 ms,表现出来的现象刚好就是重传了 15 次,超过了 timeout 值,从而断开 TCP 连接 59 | - 如果 RTT 比较大,假设 RTO 初始值计算得到的是 1000 ms,也就是第一轮的超时时间是 1 秒,那么根本不需要重传 15 次,重传总间隔就会超过 924600 ms。 60 | 61 | 最小 RTO 和最大 RTO 是在 Linux 内核中定义好了: 62 | 63 | ```c 64 | #define TCP_RTO_MAX ((unsigned)(120*HZ)) 65 | #define TCP_RTO_MIN ((unsigned)(HZ/5)) 66 | ``` 67 | 68 | Linux 2.6+ 使用 1000 毫秒的 HZ,因此`TCP_RTO_MIN`约为 200 毫秒,`TCP_RTO_MAX`约为 120 秒。 69 | 70 | 如果`tcp_retries`设置为`15`,且 RTT 比较小,那么 RTO 初始值就约等于下限 200ms,这意味着**它需要 924.6 秒**才能将断开的 TCP 连接通知给上层(即应用程序),每一轮的 RTO 增长关系如下表格: 71 | 72 | ![](https://img-blog.csdnimg.cn/img_convert/10fa6882db83aee68f246c04fcb7d760.png) 73 | 74 | ## 拔掉网线后,没有数据传输 75 | 76 | 针对拔掉网线后,没有数据传输的场景,还得看是否开启了 TCP keepalive 机制(TCP 保活机制)。 77 | 78 | 如果**没有开启** TCP keepalive 机制,在客户端拔掉网线后,并且双方都没有进行数据传输,那么客户端和服务端的 TCP 连接将会一直保持存在。 79 | 80 | 而如果**开启**了 TCP keepalive 机制,在客户端拔掉网线后,即使双方都没有进行数据传输,在持续一段时间后,TCP 就会发送探测报文: 81 | 82 | - 如果**对端是正常工作**的。当 TCP 保活的探测报文发送给对端,对端会正常响应,这样 **TCP 保活时间会被重置**,等待下一个 TCP 保活时间的到来。 83 | - 如果**对端主机宕机**(*注意不是进程崩溃,进程崩溃后操作系统在回收进程资源的时候,会发送 FIN 报文,而主机宕机则是无法感知的,所以需要 TCP 保活机制来探测对方是不是发生了主机宕机*),或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,**TCP 会报告该 TCP 连接已经死亡**。 84 | 85 | 所以,TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活。 86 | 87 | > TCP keepalive 机制具体是怎么样的? 88 | 89 | 这个机制的原理是这样的: 90 | 91 | 定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。 92 | 93 | 在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值: 94 | 95 | ```plain 96 | net.ipv4.tcp_keepalive_time=7200 97 | net.ipv4.tcp_keepalive_intvl=75 98 | net.ipv4.tcp_keepalive_probes=9 99 | ``` 100 | 101 | - tcp_keepalive_time=7200:表示保活时间是 7200 秒(2 小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制 102 | - tcp_keepalive_intvl=75:表示每次检测间隔 75 秒; 103 | - tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。 104 | 105 | 也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。 106 | 107 | ![](https://img-blog.csdnimg.cn/img_convert/46906e588260607680db43a68fe00278.png) 108 | 109 | 注意,应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 `SO_KEEPALIVE` 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。 110 | 111 | > TCP keepalive 机制探测的时间也太长了吧? 112 | 113 | 对的,是有点长。 114 | 115 | TCP keepalive 是 **TCP 层(内核态)** 实现的,它是给所有基于 TCP 传输协议的程序一个兜底的方案。 116 | 117 | 实际上,我们应用层可以自己实现一套探测机制,可以在较短的时间内,探测到对方是否存活。 118 | 119 | 比如,web 服务软件一般都会提供 `keepalive_timeout` 参数,用来指定 HTTP 长连接的超时时间。如果设置了 HTTP 长连接的超时时间是 60 秒,web 服务软件就会**启动一个定时器**,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,**定时器的时间一到,就会触发回调函数来释放该连接。** 120 | 121 | ![图片](https://img-blog.csdnimg.cn/img_convert/c881f163091a4c6427d68b7144c3a980.png) 122 | 123 | ## 总结 124 | 125 | 客户端拔掉网线后,并不会直接影响 TCP 连接状态。所以,拔掉网线后,TCP 连接是否还会存在,关键要看拔掉网线之后,有没有进行数据传输。 126 | 127 | 有数据传输的情况: 128 | 129 | - 在客户端拔掉网线后,如果服务端发送了数据报文,那么在服务端重传次数没有达到最大值之前,客户端就插回了网线,那么双方原本的 TCP 连接还是能正常存在,就好像什么事情都没有发生。 130 | - 在客户端拔掉网线后,如果服务端发送了数据报文,在客户端插回网线之前,服务端重传次数达到了最大值时,服务端就会断开 TCP 连接。等到客户端插回网线后,向服务端发送了数据,因为服务端已经断开了与客户端相同四元组的 TCP 连接,所以就会回 RST 报文,客户端收到后就会断开 TCP 连接。至此,双方的 TCP 连接都断开了。 131 | 132 | 没有数据传输的情况: 133 | 134 | - 如果双方都没有开启 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,那么客户端和服务端的 TCP 连接状态将会一直保持存在。 135 | - 如果双方都开启了 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,TCP keepalive 机制会探测到对方的 TCP 连接没有存活,于是就会断开 TCP 连接。而如果在 TCP 探测期间,客户端插回了网线,那么双方原本的 TCP 连接还是能正常存在。 136 | 137 | 除了客户端拔掉网线的场景,还有客户端「[主机宕机和进程崩溃](https://xiaolincoding.com/network/3_tcp/tcp_down_and_crash.html)」的两种场景。 138 | 139 | 第一个场景,客户端宕机这件事跟拔掉网线是一样无法被服务端的感知的,所以如果在没有数据传输,并且没有开启 TCP keepalive 机制时,,**服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态**,直到服务端重启进程。 140 | 141 | 所以,我们可以得知一个点。在没有使用 TCP 保活机制,且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态时,并不代表另一方的 TCP 连接还一定是正常的。 142 | 143 | 第二个场景,客户端的进程崩溃后,客户端的内核就会向服务端发送 FIN 报文,**与服务端进行四次挥手**。 144 | 145 | 所以,即使没有开启 TCP keepalive,且双方也没有数据交互的情况下,如果其中一方的进程发生了崩溃,这个过程操作系统是可以感知的到的,于是就会发送 FIN 报文给对方,然后与对方进行 TCP 四次挥手。 146 | 147 | 完! 148 | 149 | --- 150 | 151 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 152 | 153 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /network/5_learn/draw.md: -------------------------------------------------------------------------------- 1 | # 6.2 画图经验分享 2 | 3 | 小林写这么多篇图解文章,你们猜我收到的最多的读者问题是什么?没错,就是问我是使用什么**画图**工具,看来对这一点大家都相当好奇,那干脆不如写一篇介绍下我是怎么画图的。 4 | 5 | 如果我的文章缺少了自己画的图片,相当于失去了灵魂,技术文章本身就很枯燥,如果文章中没有几张图片,读者被劝退的概率飙飙升,剩下没被劝退的估计看着看着就睡着了。所以,精美的图片可以说是必不可少的一部分,不仅在阅读时能带来视觉的冲击,而且图片相比文字能涵盖更多的信息,不然怎会有一图胜千言的说法呢? 6 | 7 | 这时,可能有的读者会说自己不写文章呀,是不是没有必要了解画图了?我觉得这是不对,画图在我们工作中其实也是有帮助的,比如如果你想跟领导汇报一个业务流程的问题,把业务流程画出来,肯定用图的方式比用文字的方式交流起来会更有效率,更轻松些;如果你参与了一个比较复杂的项目开发,你也可以把代码的流程图给画出来,不仅能帮助自己加深理解,也能帮助后面参与的同事能更快的接手这个项目;甚至如果你要晋升级别了,演讲 PTT 里的配图也是必不可少的。 8 | 9 | 不过很多人都是纠结用什么画图工具,其实小林觉得再烂的画图工具,只要你思路清晰,确定自己要表达出什么信息,也是能把图画好的,所以不必纠结哪款画图工具,挑一款自己画起来舒服的就行了。 10 | 11 | > “小林,你说的我都懂,我就是喜欢你的画图风格嘛,你就说说你用啥画的?” 12 | 13 | 咳咳,没问题,直接坦白讲,我用的是一个在线的画图网址,地址是: 14 | - *https://draw.io* 15 | 16 | 用它的原因是使用方便和简单,当然最重要的是它完全免费,没有什么限制,甚至还能直接把图片保存到 GoogleDrive、OneDrive 和 Github,我就是保存到 Github,然后用 Github 作为我的图床。 17 | 18 | 19 | 既然要认识它,那就先来看看它长什么样子,它主要分为三个区域,从左往右的顺序是「图形选择区域、绘图区域、属性设置区域」。 20 | 21 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/正面图.png) 22 | 23 | 其中,最左边的「图形选择区域」可以选择的图案有很多种,常见的流程图、时序图、表格图都有,甚至还可以在最左下角的「更多图形」找到其他种类的图形,比如网络设备图标等。 24 | 25 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/图形选择.png) 26 | 27 | 再来,最右边「属性设置区域」可以设置文字的大小,图片颜色、线条形状等,而我最常用颜色板块是下面这三种,都是比较浅色的,这样看起来舒服些。 28 | 29 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/浅色风格2.png) 30 | 31 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/深浅色风格.png) 32 | 33 | 我最近常用的一个图形是圆角方块图,它的位置如下图,但是它默认的颜色过于深色,如果要在方框图中描述文字,则可能看不清楚,这时我会在最右侧的「属性设置区域」把方块颜色设置成浅色系列的。另外,还有一点需要注意的是,默认的字体大小比较小,我一般会调成 `16px` 大小。 34 | 35 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/常用的方块.png) 36 | 37 | 如果你不喜欢上图的带有「划痕」的圆角方块图形,可以选择下图中这个最简洁的圆角方框图形。 38 | 39 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/圆角方块图形.png) 40 | 41 | 这个简洁的圆角方框图形,再搭配颜色,能组合成很多结构图,比如我用过它组成过 CPU Cache 的结构图。 42 | 43 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/操作系统/存储结构/CPU-Cache.png) 44 | 45 | 那直角方框图形,我主要是用来组成「表格」,原因自带的表格不好看,也不方便调。 46 | 47 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/表格.png) 48 | 49 | 如果觉得直直的线条太死板,你可以把图片属性中的「*Comic*」勾上,于是就会变成歪歪扭扭的效果啦,有点像手绘风格,挺多人喜欢这种风格。 50 | 51 | 比如,我用过这种风格画过 TCP 三次握手流程的图。 52 | 53 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B%E5%92%8C%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B/14.jpg) 54 | 55 | 方块图形再加上菱形,就可以组合成简单程序流程图了,比如我画过存储器缓存流程图。 56 | 57 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/操作系统/存储结构/缓存体系.png) 58 | 59 | 所以,不要小看这些基本图形,只要构思清晰,再基本的图形,也是能构成层次分明并且好看的图。 60 | 61 | 基本的图形介绍完后,相信你画一些简单程序流程图等图形是没问题的了,接下来就是各种**图形 + 线条**的组合的了。 62 | 63 | 通过一些基本的图形组合,你还可以画出时序图,时序图可以用来描述多个对象之间的交互流程,比如我画过多个线程获取互斥锁的时序图。 64 | 65 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/操作系统/锁/互斥锁工作流程.png) 66 | 67 | 再来,为了更好表达零拷贝技术的过程,那么用图的方式会更清晰。 68 | 69 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E9%9B%B6%E6%8B%B7%E8%B4%9D/senfile-%E9%9B%B6%E6%8B%B7%E8%B4%9D.png) 70 | 71 | 前面也提到,图形不只是简单图形,还有其他自带的设备类图形,比如我用网络设备图画过单播、广播、多播通信的区别图。 72 | 73 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/IP/13.jpg) 74 | 75 | 你要说,我画过最复杂的图,那就是写 TCP 流量控制的时候,把整个交互过程 + 文字描述 + 滑动窗口状况都画出来了,现在回想起来还是觉得累人。 76 | 77 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%AF%E9%9D%A0%E7%89%B9%E6%80%A7/22.jpg) 78 | 79 | 还有好多好多,我就比一一列举,这半年下来,小林至少画了 `500+` 张图了,每一张图其实还是挺费时间的,相信画过图的朋友后,都能体会到这种感觉了。但没办法,谁叫小林是图解工具人呢,画图可以更好的诠释文章内容,但最重要的是,把你们吸引过来了,这是件让我非常高兴的事情,也是让我感觉画图这个事情值得认真做。 80 | 81 | 另外,细心的读者也发现了,小林贴代码的时候,使用的是图片的形式,原因是代码通常都是比较长,在手机看文章用图片的呈现的方式会更舒服清晰。 82 | 83 | 在这里也推荐下这个代码截图网址: 84 | - *https://carbon.now.sh/* 85 | 86 | 网站页面如下图,代码显示的效果是不是很美观? 87 | 88 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/carbon.png) 89 | 90 | 文字的分享有局限性,关键还是要你自己动手摸索摸索,形成自己一套画图的方法论,练习的时候可以先从模仿画起,后面再结合工作或文章的需求画出自己心中的那个图。 -------------------------------------------------------------------------------- /network/5_learn/learn_network.md: -------------------------------------------------------------------------------- 1 | # 6.1 计算机网络怎么学? 2 | 3 | 计算机网络相比操作系统好学非常多,因为计算机网络不抽象,你要想知道网络中的细节,你都可以通过抓包来分析,而且不管是手机、个人电脑和服务器,它们所使用的计算网络协议是一致的。 4 | 5 | 也就是说,计算机网络不会因为设备的不同而不同,大家都遵循这一套「规则」来相互通信,这套规则就是 TCP/IP 网络模型。 6 | 7 | ![OSI 参考模型与 TCP/IP 的关系](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B%E5%92%8C%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B/7.jpg) 8 | 9 | TCP/IP 网络参考模型共有 `4` 层,其中需要我们熟练掌握的是应用层、传输层和网络层,至于网络接口层(数据链路层和物理层)我们只需要做简单的了解就可以了。 10 | 11 | 对于应用层,当然重点要熟悉最常见的 [HTTP 和 HTTPS](https://mp.weixin.qq.com/s/bUy220-ect00N4gnO0697A),传输层 TCP 和 UDP 都要熟悉,网络层要熟悉 [IPv4](https://mp.weixin.qq.com/s/bUy220-ect00N4gnO0697A),IPv6 可以做简单点了解。 12 | 13 | 14 | 我觉得学习一个东西,就从我们常见的事情开始着手。 15 | 16 | 比如,ping 命令可以说在我们判断网络环境的时候,最常使用的了,你可以先把你电脑 ping 你舍友或同事的电脑的过程中发生的事情都搞明白,这样就基本知道一个数据包是怎么转发的了,于是你就知道了网络层、数据链路层和物理层之间是如何工作,如何相互配合的了。 17 | 18 | 19 | 搞明白了 ping 过程,我相信你学起 HTTP 请求过程的时候,会很快就能掌握了,因为网络层以下的工作方式,你在学习 ping 的时候就已经明白了,这时就只需要认真掌握传输层中的 TCP 和应用层中的 HTTP 协议,就能搞明白[访问网页的整个过程](https://mp.weixin.qq.com/s/iSZp41SRmh5b2bXIvzemIw)了,这也是面试常见的题目了,毕竟它能考察你网络知识的全面性。 20 | 21 | 重中之重的知识就是 TCP 了,TCP 不管是[建立连接、断开连接](https://mp.weixin.qq.com/s/tH8RFmjrveOmgLvk9hmrkw)的过程,还是数据传输的过程,都不能放过,针对数据可靠传输的特性,又可以拆解为[超时重传、流量控制、滑动窗口、拥塞控制](https://mp.weixin.qq.com/s/Tc09ovdNacOtnMOMeRc_uA)等等知识点,学完这些只能算对 TCP 有个「**感性**」的认识,另外我们还得知道 Linux 提供的 [TCP 内核的参数](https://mp.weixin.qq.com/s/fjnChU3MKNc_x-Wk7evLhg)的作用,这样才能从容地应对工作中遇到的问题。 22 | 23 | 接下来,推荐我看过并觉得不错的计算机网络相关的书籍和视频。 24 | 25 | ## 入门系列 26 | 27 | 此系列针对没有任何计算机基础的朋友,如果已经对计算机轻车熟路的大佬,也不要忽略,不妨看看我推荐的正确吗。 28 | 29 | 如果你要入门 HTTP,首先最好书籍就是《**图解 HTTP**》了,作者真的做到完完全全的「图解」,小林的图解功夫还是从这里偷学到不少,书籍不厚,相信优秀的你,几天就可以看完了。 30 | 31 | ![《图解 HTTP》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/其他/图解HTTP.jpg) 32 | 33 | 34 | 如果要入门 TCP/IP 网络模型,我推荐的是《**图解 TCP/IP**》,这本书也是以大量的图文来介绍了 TCP/IP 网络模式的每一层,但是这个书籍的顺序不是从「应用层 —> 物理层」,而是从「物理层 -> 应用层」顺序开始讲的,这一点我觉得不太好,这样一上来就把最枯燥的部分讲了,很容易就被劝退了,所以我建议先跳过前面几个章节,先看网络层和传输层的章节,然后再回头看前面的这几个章节。 35 | 36 | ![《图解 TCP/IP》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E5%9B%BE%E8%A7%A3TCPIP.png) 37 | 38 | 39 | 40 | 另外,你想了解网络是怎么传输,那我推荐《**网络是怎样连接的**》,这本书相对比较全面的把访问一个网页的发生的过程讲解了一遍,其中关于电信等运营商是怎么传输的,这部分你可以跳过,当然你感兴趣也可以看,只是我觉得没必要看。 41 | 42 | 43 | 44 | ![《网络是怎样连接的》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E7%BD%91%E7%BB%9C%E6%98%AF%E6%80%8E%E4%B9%88%E8%BF%9E%E6%8E%A5%E7%9A%84.png) 45 | 46 | 47 | 如果你觉得书籍过于枯燥,你可以结合 B 站《**计算机网络微课堂**》视频一起学习,这个视频是湖南科技大学老师制作的,PPT 的动图是我见过做的最用心的了,一看就懂的佳作。 48 | 49 | ![《计算机网络微课堂》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/其他/计算机网络微课堂.png) 50 | 51 | 52 | > B 站视频地址:https://www.bilibili.com/video/BV1c4411d7jb?p=1 53 | 54 | 55 | 56 | ## 深入学习系列 57 | 58 | 看完入门系列,相信你对计算机网络已经有个大体的认识了,接下来我们也不能放慢脚步,快马加鞭,借此机会继续深入学习,因为隐藏在背后的细节还是很多的。 59 | 60 | 对于 TCP/IP 网络模型深入学习的话,推荐《**计算机网络 - 自顶向下方法**》,这本书是从我们最熟悉 HTTP 开始说起,一层一层的说到最后物理层的,有种挖地洞的感觉,这样的内容编排顺序相对是比较合理的。 61 | 62 | ![《计算机网络 - 自顶向下方法》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/其他/计算机网络自定向下.png) 63 | 64 | 65 | 66 | 但如果要深入 TCP,前面的这些书还远远不够,赋有计算机网络圣经的之说的《**TCP/IP 详解 卷一:协议**》这本书,是进一步深入学习的好资料,这本书的作者用各种实验的方式来细说各种协议,但不得不说,这本书真的很枯燥,当时我也啃的很难受,但是它质量是真的很高,这本书我只看了 TCP 部分,其他部分你可以选择性看,但是你一定要过几遍这本书的 TCP 部分,涵盖的内容非常全且细。 67 | 68 | 69 | ![《TCP/IP 详解 卷一:协议》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/TCPIP%E5%8D%8F%E8%AE%AE%E8%AF%A6%E8%A7%A3.png) 70 | 71 | 72 | 要说我看过最好的 TCP 资料,那必定是《**The TCP/IP GUIDE**》这本书了,目前只有英文版本的,而且有个专门的网址可以白嫖看这本书的内容,图片都是彩色,看起来很舒服很鲜明,小林之前写的 TCP 文章不少案例和图片都是参考这里的,这本书精华部分就是把 TCP 滑动窗口和流量控制说的超级明白,很可惜拥塞控制部分说的不多。 73 | 74 | ![《The TCP/IP GUIDE》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/TCPIP%20GUIDE.png) 75 | 76 | 77 | 78 | > 白嫖站点:http://www.tcpipguide.com/free/t_TCPSlidingWindowAcknowledgmentSystemForDataTranspo-6.htm 79 | 80 | 81 | 82 | 当然,计算机网络最牛逼的资料,那必定 **RFC 文档**,它可以称为计算机网络世界的「法规」,也是最新、最权威和最正确的地方了,困惑大家的 TCP 为什么三次握手和四次挥手,其实在 RFC 文档几句话就说明白了。 83 | 84 | > TCP 协议的 RFC 文档:https://datatracker.ietf.org/doc/rfc1644/ 85 | 86 | 87 | ## 实战系列 88 | 89 | 90 | 在学习书籍资料的时候,不管是 TCP、UDP、ICMP、DNS、HTTP、HTTPS 等协议,最好都可以亲手尝试抓数据报,接着可以用 [Wireshark 工具](https://mp.weixin.qq.com/s/bHZ2_hgNQTKFZpWMCfUH9A)看每一个数据报文的信息,这样你会觉得计算机网络没有想象中那么抽象了,因为它们被你「抓」出来了,并毫无保留地显现在你面前了,于是你就可以肆无忌惮地「扒开」它们,看清它们每一个头信息。 91 | 92 | 那在这里,我也给你推荐 2 本关于 Wireshark 网络分析的书,这两本书都是同一个作者,书中的案例都是源于作者工作中的实际的案例,作者的文笔相当有趣,看起来堪比小说一样爽,相信你不用一个星期 2 本都能看完了。 93 | 94 | ![《Wireshark 网络分析就这么简单》与《Wireshark 网络分析的艺术》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/其他/wireshark书.png) 95 | 96 | ## 最后 97 | 98 | 文中推荐的书,小林都已经把电子书整理好给大家了,只需要在小林的公众号后台回复「**我要学习**」,即可获取百度网盘下载链接。 99 | 100 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /network/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 图解网络介绍 3 | 4 | 大家好,我是小林,是《图解网络》的作者,本站的内容都是整理于我[公众号](https://mp.weixin.qq.com/s/FYH1I8CRsuXDSybSGY_AFA)里的图解文章。 5 | 6 | 还没关注的朋友,可以微信搜索「**小林 coding**」,关注我的公众号,**后续最新版本的 PDF 会在我的公众号第一时间发布**,而且会有更多其他系列的图解文章,比如操作系统、计算机组成、数据库、算法等等。 7 | 8 | 简单介绍下《图解网络》,整个内容共有 **`20W` 字 + `500` 张图**,每一篇都自己手绘了很多图,目的也很简单,击破大家对于「八股文」的恐惧。 9 | 10 | ## 适合什么群体? 11 | 12 | 《图解网络》写的网络知识主要是**面向程序员**的,因为小林本身也是个程序员,所以涉及到的知识主要是关于程序员日常工作或者面试的网络知识。 13 | 14 | 非常适合有一点网络基础,但是又不怎么扎实,或者知识点串不起来的同学,说白**这本图解网络就是为了拯救半桶水的同学而出来的**。 15 | 16 | 因为小林写的图解网络就四个字,**通俗易懂**! 17 | 18 | 相信你在看这本图解网络的时候,你心里的感受会是: 19 | 20 | - 「卧槽,原来是这样,大学老师教知识原来是这么理解」 21 | - 「卧槽,我的网络知识串起来了」 22 | - 「卧槽,我感觉面试稳了」 23 | - 「卧槽,相见恨晚」 24 | 25 | 当然,也适合面试突击网络知识时拿来看。图解网络里的内容基本是面试常见的协议,比如 HTTP、HTTPS、TCP、UDP、IP 等等,也有很多面试常问的问题,比如: 26 | 27 | - TCP 为什么三次握手?四次挥手? 28 | - TCP 为什么要有 TIME_WAIT 状态? 29 | - TCP 为什么是可靠传输协议,而 UDP 不是? 30 | - 键入网址到网页显示,期间发生了什么? 31 | - HTTPS 握手过程是怎样的? 32 | - ……. 33 | 34 | 不敢说 100 % 涵盖了面试的网络问题,但是至少 90% 是有的,而且内容的深度应对大厂也是绰绰有余,有非常多的读者跑来感激小林的图解网络,帮助他们拿到了国内很多一线大厂的 offer。 35 | 36 | ## 要怎么阅读? 37 | 38 | 很诚恳的告诉你,《图解网络》不是教科书,而是我写的图解网络文章的整合,所以肯定是没有教科书那么细致和全面,当然也就不会有很多废话,都是直击重点,不绕弯,而且有的知识点书上看不到。 39 | 40 | 阅读的顺序可以不用从头读到尾,你可以根据你想要了解的知识点,通过本站的搜索功能,去看哪个章节的内容就好,可以随意阅读任何章节。 41 | 42 | 本站的左侧边拦就是《图解网络》的目录结构(别看篇章不多,每一章都是很长很长的文章哦 :laughing:): 43 | 44 | - **网络基础篇** :point_down: 45 | - [TCP/IP 网络模型有哪几层?](/network/1_base/tcp_ip_model.md) 46 | - [键入网址到网页显示,期间发生了什么?](/network/1_base/what_happen_url.md) 47 | - [Linux 系统是如何收发网络包的?](/network/1_base/how_os_deal_network_package.md) 48 | - **HTTP 篇** :point_down: 49 | - [HTTP 常见面试题](/network/2_http/http_interview.md) 50 | - [HTTP/1.1 如何优化?](/network/2_http/http_optimize.md) 51 | - [HTTPS RSA 握手解析](/network/2_http/https_rsa.md) 52 | - [HTTPS ECDHE 握手解析](/network/2_http/https_ecdhe.md) 53 | - [HTTPS 如何优化?](/network/2_http/https_optimize.md) 54 | - [HTTP/2 牛逼在哪?](/network/2_http/http2.md) 55 | - [HTTP/3 强势来袭](/network/2_http/http3.md) 56 | - [既然有 HTTP 协议,为什么还要有 RPC?](/network/2_http/http_rpc.md) 57 | - [既然有 HTTP 协议,为什么还要有 websocket?](/network/2_http/http_websocket.md) 58 | - **TCP 篇** :point_down: 59 | - [TCP 三次握手与四次挥手面试题](/network/3_tcp/tcp_interview.md) 60 | - [TCP 重传、滑动窗口、流量控制、拥塞控制](/network/3_tcp/tcp_feature.md) 61 | - [TCP 实战抓包分析](/network/3_tcp/tcp_tcpdump.md) 62 | - [TCP 半连接队列和全连接队列](/network/3_tcp/tcp_queue.md) 63 | - [如何优化 TCP?](/network/3_tcp/tcp_optimize.md) 64 | - [如何理解是 TCP 面向字节流协议?](/network/3_tcp/tcp_stream.md) 65 | - [为什么 TCP 每次建立连接时,初始化序列号都要不一样呢?](/network/3_tcp/isn_deff.md) 66 | - [SYN 报文什么时候情况下会被丢弃?](/network/3_tcp/syn_drop.md) 67 | - [四次挥手中收到乱序的 FIN 包会如何处理?](/network/3_tcp/out_of_order_fin.md) 68 | - [在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么?](/network/3_tcp/time_wait_recv_syn.md) 69 | - [TCP 连接,一端断电和进程崩溃有什么区别?](/network/3_tcp/tcp_down_and_crash.md) 70 | - [拔掉网线后,原本的 TCP 连接还存在吗?](/network/3_tcp/tcp_unplug_the_network_cable.md) 71 | - [tcp_tw_reuse 为什么默认是关闭的?](/network/3_tcp/tcp_tw_reuse_close.md) 72 | - [HTTPS 中 TLS 和 TCP 能同时握手吗?](/network/3_tcp/tcp_tls.md) 73 | - [TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?](/network/3_tcp/tcp_http_keepalive.md) 74 | - [TCP 有什么缺陷?](/network/3_tcp/tcp_problem.md) 75 | - [如何基于 UDP 协议实现可靠传输?](/network/3_tcp/quic.md) 76 | - [TCP 和 UDP 可以使用同一个端口吗?](/network/3_tcp/port.md) 77 | - [服务端没有 listen,客户端发起连接建立,会发生什么?](/network/3_tcp/tcp_no_listen.md) 78 | - [没有 accept,可以建立 TCP 连接吗?](/network/3_tcp/tcp_no_accpet.md) 79 | - [用了 TCP 协议,数据一定不会丢吗?](/network/3_tcp/tcp_drop.md) 80 | - [TCP 四次挥手,可以变成三次吗?](/network/3_tcp/tcp_three_fin.md) 81 | - **IP 篇** :point_down: 82 | - [IP 基础知识全家桶](/network/4_ip/ip_base.md) 83 | - [ping 的工作原理](/network/4_ip/ping.md) 84 | - [断网了,还能 ping 通 127.0.0.1 吗?](/network/4_ip/ping_lo.md) 85 | - **学习心得** :point_down: 86 | - [计算机网络怎么学?](/network/5_learn/learn_network.md) 87 | - [画图经验分享](/network/5_learn/draw.md) 88 | 89 | ## 质量如何? 90 | 91 | 图解网络的质量小林说的不算,读者说的算! 92 | 93 | 图解网络的第一个版本自去年发布以来,每隔一段时间,就会有不少的读者跑来感激小林。 94 | 95 | 他们说看了我的图解网络,轻松应对大厂的网络面试题,而且每次面试时问到网络问题,他们一点都不慌,甚至暗暗窃喜。 96 | 97 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/160f55b965cf4c42ba160e327178a783.png) 98 | 99 | ## 有错误怎么办? 100 | 101 | 小林是个手残党,时常写出错别字。 102 | 103 | 如果你在学习的过程中,**如果你发现有任何错误或者疑惑的地方,欢迎你通过邮箱或者底部留言给小林**,勘误邮箱:xiaolincoding@163.com 104 | 105 | 小林抽时间会逐个修正,然后发布新版本的图解网络 PDF,一起迭代出更好的图解网络! 106 | 107 | 新的图解文章都在公众号首发,别忘记关注了哦!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 108 | 109 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/其他/公众号介绍.png) 110 | 111 | -------------------------------------------------------------------------------- /os/10_learn/draw.md: -------------------------------------------------------------------------------- 1 | # 11.2 画图经验分享 2 | 3 | 小林写这么多篇图解文章,你们猜我收到的最多的读者问题是什么?没错,就是问我是使用什么**画图**工具,看来对这一点大家都相当好奇,那干脆不如写一篇介绍下我是怎么画图的。 4 | 5 | 如果我的文章缺少了自己画的图片,相当于失去了灵魂,技术文章本身就很枯燥,如果文章中没有几张图片,读者被劝退的概率飙飙升,剩下没被劝退的估计看着看着就睡着了。所以,精美的图片可以说是必不可少的一部分,不仅在阅读时能带来视觉的冲击,而且图片相比文字能涵盖更多的信息,不然怎会有一图胜千言的说法呢? 6 | 7 | 这时,可能有的读者会说自己不写文章呀,是不是没有必要了解画图了?我觉得这是不对,画图在我们工作中其实也是有帮助的,比如如果你想跟领导汇报一个业务流程的问题,把业务流程画出来,肯定用图的方式比用文字的方式交流起来会更有效率,更轻松些;如果你参与了一个比较复杂的项目开发,你也可以把代码的流程图给画出来,不仅能帮助自己加深理解,也能帮助后面参与的同事能更快的接手这个项目;甚至如果你要晋升级别了,演讲 PTT 里的配图也是必不可少的。 8 | 9 | 不过很多人都是纠结用什么画图工具,其实小林觉得再烂的画图工具,只要你思路清晰,确定自己要表达出什么信息,也是能把图画好的,所以不必纠结哪款画图工具,挑一款自己画起来舒服的就行了。 10 | 11 | > “小林,你说的我都懂,我就是喜欢你的画图风格嘛,你就说说你用啥画的?” 12 | 13 | 咳咳,没问题,直接坦白讲,我用的是一个在线的画图网址,地址是: 14 | - *https://draw.io* 15 | 16 | 用它的原因是使用方便和简单,当然最重要的是它完全免费,没有什么限制,甚至还能直接把图片保存到 GoogleDrive、OneDrive 和 Github,我就是保存到 Github,然后用 Github 作为我的图床。 17 | 18 | 19 | 既然要认识它,那就先来看看它长什么样子,它主要分为三个区域,从左往右的顺序是「图形选择区域、绘图区域、属性设置区域」。 20 | 21 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/正面图.png) 22 | 23 | 其中,最左边的「图形选择区域」可以选择的图案有很多种,常见的流程图、时序图、表格图都有,甚至还可以在最左下角的「更多图形」找到其他种类的图形,比如网络设备图标等。 24 | 25 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/图形选择.png) 26 | 27 | 再来,最右边「属性设置区域」可以设置文字的大小,图片颜色、线条形状等,而我最常用颜色板块是下面这三种,都是比较浅色的,这样看起来舒服些。 28 | 29 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/浅色风格.png) 30 | 31 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/浅色风格2.png) 32 | 33 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/深浅色风格.png) 34 | 35 | 我最近常用的一个图形是圆角方块图,它的位置如下图,但是它默认的颜色过于深色,如果要在方框图中描述文字,则可能看不清楚,这时我会在最右侧的「属性设置区域」把方块颜色设置成浅色系列的。另外,还有一点需要注意的是,默认的字体大小比较小,我一般会调成 `16px` 大小。 36 | 37 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/常用的方块.png) 38 | 39 | 如果你不喜欢上图的带有「划痕」的圆角方块图形,可以选择下图中这个最简洁的圆角方框图形。 40 | 41 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/圆角方块图形.png) 42 | 43 | 这个简洁的圆角方框图形,再搭配颜色,能组合成很多结构图,比如我用过它组成过 CPU Cache 的结构图。 44 | 45 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/操作系统/存储结构/CPU-Cache.png) 46 | 47 | 那直角方框图形,我主要是用来组成「表格」,原因自带的表格不好看,也不方便调。 48 | 49 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/表格.png) 50 | 51 | 如果觉得直直的线条太死板,你可以把图片属性中的「*Comic*」勾上,于是就会变成歪歪扭扭的效果啦,有点像手绘风格,挺多人喜欢这种风格。 52 | 53 | 比如,我用过这种风格画过 TCP 三次握手流程的图。 54 | 55 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B%E5%92%8C%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B/14.jpg) 56 | 57 | 方块图形再加上菱形,就可以组合成简单程序流程图了,比如我画过存储器缓存流程图。 58 | 59 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/操作系统/存储结构/缓存体系.png) 60 | 61 | 所以,不要小看这些基本图形,只要构思清晰,再基本的图形,也是能构成层次分明并且好看的图。 62 | 63 | 基本的图形介绍完后,相信你画一些简单程序流程图等图形是没问题的了,接下来就是各种**图形 + 线条**的组合的了。 64 | 65 | 通过一些基本的图形组合,你还可以画出时序图,时序图可以用来描述多个对象之间的交互流程,比如我画过多个线程获取互斥锁的时序图。 66 | 67 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/操作系统/锁/互斥锁工作流程.png) 68 | 69 | 再来,为了更好表达零拷贝技术的过程,那么用图的方式会更清晰。 70 | 71 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E9%9B%B6%E6%8B%B7%E8%B4%9D/senfile-%E9%9B%B6%E6%8B%B7%E8%B4%9D.png) 72 | 73 | 前面也提到,图形不只是简单图形,还有其他自带的设备类图形,比如我用网络设备图画过单播、广播、多播通信的区别图。 74 | 75 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/IP/13.jpg) 76 | 77 | 你要说,我画过最复杂的图,那就是写 TCP 流量控制的时候,把整个交互过程 + 文字描述 + 滑动窗口状况都画出来了,现在回想起来还是觉得累人。 78 | 79 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%AF%E9%9D%A0%E7%89%B9%E6%80%A7/22.jpg) 80 | 81 | 还有好多好多,我就比一一列举,这半年下来,小林至少画了 `500+` 张图了,每一张图其实还是挺费时间的,相信画过图的朋友后,都能体会到这种感觉了。但没办法,谁叫小林是图解工具人呢,画图可以更好的诠释文章内容,但最重要的是,把你们吸引过来了,这是件让我非常高兴的事情,也是让我感觉画图这个事情值得认真做。 82 | 83 | 另外,细心的读者也发现了,小林贴代码的时候,使用的是图片的形式,原因是代码通常都是比较长,在手机看文章用图片的呈现的方式会更舒服清晰。 84 | 85 | 在这里也推荐下这个代码截图网址: 86 | - *https://carbon.now.sh/* 87 | 88 | 网站页面如下图,代码显示的效果是不是很美观? 89 | 90 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/carbon.png) 91 | 92 | 文字的分享有局限性,关键还是要你自己动手摸索摸索,形成自己一套画图的方法论,练习的时候可以先从模仿画起,后面再结合工作或文章的需求画出自己心中的那个图。 -------------------------------------------------------------------------------- /os/10_learn/learn_os.md: -------------------------------------------------------------------------------- 1 | # 11.1 操作系统怎么学? 2 | 3 | 操作系统真的可以说是 `Super Man`,它为了我们做了非常厉害的事情,以至于我们根本察觉不到,只有通过学习它,我们才能深刻体会到它的精妙之处,甚至会被计算机科学家设计思想所震撼,有些思想实际上也是可以应用于我们工作开发中。 4 | 5 | 操作系统比较重要的四大模块,分别是[内存管理](https://mp.weixin.qq.com/s/HJB_ATQFNqG82YBCRr97CA)、[进程管理](https://mp.weixin.qq.com/s/YXl6WZVzRKCfxzerJWyfrg)、[文件系统管理](https://mp.weixin.qq.com/s/qJdoXTv_XS_4ts9YuzMNIw)、[输入输出设备管理](https://mp.weixin.qq.com/s/04BkLtnPBmmx6CtdQPXiRA)。这是我学习操作系统的顺序,也是我推荐给大家的学习顺序,因为内存管理不仅是最重要、最难的模块,也是和其他模块关联性最大的模块,先把它搞定,后续的模块学起来我认为会相对轻松一些。 6 | 7 | 学习的过程中,你可能会遇到很多「虚拟」的概念,比如虚拟内存、虚拟文件系统,实际上它们的本质上都是一样的,都是**向下屏蔽差异,向上提供统一的东西**,以方便我们程序员使用。 8 | 9 | 还有,你也遇到各种各样的[调度算法](https://mp.weixin.qq.com/s/JWj6_BF9Xc84kQcyx6Nf_g),在这里你可以看到数据结构与算法的魅力,重要的是我们要理解为什么要提出那么多调度算法,你当然可以说是为了更快更有效率,但是因什么问题而因此引入新算法的这个过程,更是我们重点学习的地方。 10 | 11 | 你也会开始明白进程与线程最大的区别在于上下文切换过程中,**线程不用切换虚拟内存**,因为同一个进程内的线程都是共享虚拟内存空间的,线程就单这一点不用切换,就相比进程上下文切换的性能开销减少了很多。由于虚拟内存与物理内存的映射关系需要查询页表,页表的查询是很慢的过程,因此会把常用的地址映射关系缓存在 TLB 里的,这样便可以提高页表的查询速度,如果发生了进程切换,那 TLB 缓存的地址映射关系就会失效,缓存失效就意味着命中率降低,于是虚拟地址转为物理地址这一过程就会很慢。 12 | 13 | 14 | 你也开始不会傻傻的认为 read 或 write 之后数据就直接写到硬盘了,更不会觉得多次操作 read 或 write 方法性能会很低,因为你发现操作系统会有个「**磁盘高速缓冲区**」,它已经帮我们做了缓存的工作,它会预读数据、缓存最近访问的数据,以及使用 I/O 调度算法来合并和排队磁盘调度 I/O,这些都是为了减少操作系统对磁盘的访问频率。 15 | 16 | …… 17 | 18 | 还有太多太多了,我在这里就不赘述了,剩下的就交给你们在学习操作系统的途中去探索和发现了。 19 | 20 | 21 | 还有一点需要注意,学操作系统的时候,不要误以为它是在说 Linux 操作系统,这也是我初学的时候犯的一个错误,操作系统是集合大多数操作系统实现的思想,跟实际具体实现的 Linux 操作系统多少都会有点差别,如果要想 Linux 操作系统的具体实现方式,可以选择看 Linux 内核相关的资料,但是在这之前你先掌握了操作系统的基本知识,这样学起来才能事半功倍。 22 | 23 | 24 | 25 | 26 | ## 入门系列 27 | 28 | 对于没学过操作系统的小白,我建议学的时候,不要直接闷头看书。相信我,你不用几分钟就会打退堂鼓,然后就把厚厚的书拿去垫显示器了,从此再无后续,毕竟直接看书太特喵的枯燥了,当然不如用来垫显示器玩游戏来着香。 29 | 30 | B 站关于操作系统课程资源很多,我在里面也看了不同老师讲的课程,觉得比较好的入门级课程是《**操作系统 - 清华大学**》,该课程由清华大学老师向勇和陈渝授课,虽然我们上不了清华大学,但是至少我们可以在网上选择听清华大学的课嘛。课程授课的顺序,就如我前面推荐的学习顺序:「内存管理 -> 进程管理 -> 文件系统管理 -> 输入输出设备管理」。 31 | 32 | ![《操作系统 - 清华大学》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F-%E6%B8%85%E5%8D%8E%E5%A4%A7%E5%AD%A6.png) 33 | 34 | > B 站清华大学操作系统视频地址:https://www.bilibili.com/video/BV1js411b7vg?from=search&seid=2361361014547524697 35 | 36 | 该清华大学的视频教学搭配的书应该是《**现代操作系统**》,你可以视频和书籍两者结合一起学,比如看完视频的内存管理,然后就看书上对应的章节,这样相比直接啃书相对会比较好。 37 | 38 | 39 | ![《现代操作系统》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E7%8E%B0%E4%BB%A3%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.png) 40 | 41 | 清华大学的操作系统视频课讲的比较精炼,涉及到的内容没有那么细,《**操作系统 - 哈工大**》李治军老师授课的视频课程相对就会比较细节,老师会用 Linux 内核代码的角度带你进一步理解操作系统,也会用生活小例子帮助你理解。 42 | 43 | ![《操作系统 - 哈工大》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F-%E5%93%88%E5%B7%A5%E5%A4%A7.png) 44 | 45 | > B 站哈工大操作系统视频地址:https://www.bilibili.com/video/BV1d4411v7u7?from=search&seid=2361361014547524697 46 | 47 | 48 | ## 深入学习系列 49 | 50 | 《现代操作系统》这本书我感觉缺少比较多细节,说的还是比较笼统,而且书也好无聊。 51 | 52 | 推荐一个说的更细的操作系统书 —— 《**操作系统导论**》,这本书不仅告诉你 What,还会告诉你 How,书的内容都是循序渐进,层层递进的,阅读起来还是觉得挺有意思的,这本书的内存管理和并发这两个部分说的很棒,这本书的中文版本我也没找到资源,不过微信读书可以免费看这本书。 53 | 54 | ![《操作系统导论》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AF%BC%E8%AE%BA.png) 55 | 56 | 当然,少不了这本被称为神书的《**深入理解计算机系统**》,豆瓣评分高达 `9.8` 分,这本书严格来说不算操作系统书,它是以程序员视角理解计算机系统,不只是涉及到操作系统,还涉及到了计算机组成、C 语言、汇编语言等知识,是一本综合性比较强的书。 57 | 58 | ![《深入理解计算机系统》](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F.jpg) 59 | 60 | 它告诉我们计算机是如何设计和工作的,操作系统有哪些重点,它们的作用又是什么,这本书的目标其实便是要讲清楚原理,但并不会把某个话题挖掘地过于深入,过于细节。看看这本书后,我们就可以对计算机系统各组件的工作方式有了理性的认识。在一定程度上,其实它是在锻炼一种思维方式 —— 计算思维。 61 | 62 | 63 | ---- 64 | 65 | ## 最后 66 | 67 | 文中推荐的书,小林都已经把电子书整理好给大家了,只需要在小林的公众号后台回复「**我要学习**」,即可获取百度网盘下载链接。 68 | 69 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /os/1_hardware/soft_interrupt.md: -------------------------------------------------------------------------------- 1 | # 2.6 什么是软中断? 2 | 3 | 今日的技术主题:**什么是软中断?**。 4 | 5 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/操作系统/软中断/软中断提纲.png) 6 | 7 | --- 8 | 9 | ## 中断是什么? 10 | 11 | 先来看看什么是中断?在计算机中,中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求。 12 | 13 | 这样的解释可能过于学术了,容易云里雾里,我就举个生活中取外卖的例子。 14 | 15 | 小林中午搬完砖,肚子饿了,点了份白切鸡外卖,这次我带闪了,没有被某团大数据杀熟。虽然平台上会显示配送进度,但是我也不能一直傻傻地盯着呀,时间很宝贵,当然得去干别的事情,等外卖到了配送员会通过「电话」通知我,电话响了,我就会停下手中地事情,去拿外卖。 16 | 17 | 18 | 这里的打电话,其实就是对应计算机里的中断,没接到电话的时候,我可以做其他的事情,只有接到了电话,也就是发生中断,我才会停下当前的事情,去进行另一个事情,也就是拿外卖。 19 | 20 | 从这个例子,我们可以知道,中断是一种异步的事件处理机制,可以提高系统的并发处理能力。 21 | 22 | 操作系统收到了中断请求,会打断其他进程的运行,所以**中断请求的响应程序,也就是中断处理程序,要尽可能快的执行完,这样可以减少对正常进程运行调度地影响。** 23 | 24 | 而且,中断处理程序在响应中断时,可能还会「临时关闭中断」,这意味着,如果当前中断处理程序没有执行完之前,系统中其他的中断请求都无法被响应,也就说中断有可能会丢失,所以中断处理程序要短且快。 25 | 26 | 27 | 还是回到外卖的例子,小林到了晚上又点起了外卖,这次为了犒劳自己,共点了两份外卖,一份小龙虾和一份奶茶,并且是由不同地配送员来配送,那么问题来了,当第一份外卖送到时,配送员给我打了长长的电话,说了一些杂七杂八的事情,比如给个好评等等,但如果这时另一位配送员也想给我打电话。 28 | 29 | 很明显,这时第二位配送员因为我在通话中(相当于关闭了中断响应),自然就无法打通我的电话,他可能尝试了几次后就走掉了(相当于丢失了一次中断)。 30 | 31 | --- 32 | 33 | ## 什么是软中断? 34 | 35 | 前面我们也提到了,中断请求的处理程序应该要短且快,这样才能减少对正常进程运行调度地影响,而且中断处理程序可能会暂时关闭中断,这时如果中断处理程序执行时间过长,可能在还未执行完中断处理程序前,会丢失当前其他设备的中断请求。 36 | 37 | 那 Linux 系统**为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」**。 38 | 39 | - **上半部用来快速处理中断**,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。 40 | - **下半部用来延迟处理上半部未完成的工作**,一般以「内核线程」的方式运行。 41 | 42 | 前面的外卖例子,由于第一个配送员长时间跟我通话,则导致第二位配送员无法拨通我的电话,其实当我接到第一位配送员的电话,可以告诉配送员说我现在下楼,剩下的事情,等我们见面再说(上半部),然后就可以挂断电话,到楼下后,在拿外卖,以及跟配送员说其他的事情(下半部)。 43 | 44 | 这样,第一位配送员就不会占用我手机太多时间,当第二位配送员正好过来时,会有很大几率拨通我的电话。 45 | 46 | 再举一个计算机中的例子,常见的网卡接收网络包的例子。 47 | 48 | 网卡收到网络包后,通过 DMA 方式将接收到的数据写入内存,接着会通过**硬件中断**通知内核有新的数据到了,于是内核就会调用对应的中断处理程序来处理该事件,这个事件的处理也是会分成上半部和下半部。 49 | 50 | 上部分要做的事情很少,会先禁止网卡中断,避免频繁硬中断,而降低内核的工作效率。接着,内核会触发一个**软中断**,把一些处理比较耗时且复杂的事情,交给「软中断处理程序」去做,也就是中断的下半部,其主要是需要从内存中找到网络数据,再按照网络协议栈,对网络数据进行逐层解析和处理,最后把数据送给应用程序。 51 | 52 | 53 | 所以,中断处理程序的上部分和下半部可以理解为: 54 | 55 | - **上半部直接处理硬件请求,也就是硬中断**,主要是负责耗时短的工作,特点是快速执行; 56 | - **下半部是由内核触发,也就说软中断**,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行; 57 | 58 | 59 | 还有一个区别,硬中断(上半部)是会打断 CPU 正在执行的任务,然后立即执行中断处理程序,而软中断(下半部)是以内核线程的方式执行,并且每一个 CPU 都对应一个软中断内核线程,名字通常为「ksoftirqd/CPU 编号」,比如 0 号 CPU 对应的软中断内核线程的名字是 `ksoftirqd/0` 60 | 61 | 不过,软中断不只是包括硬件设备中断处理程序的下半部,一些内核自定义事件也属于软中断,比如内核调度等、RCU 锁(内核里常用的一种锁)等。 62 | 63 | --- 64 | 65 | ### 系统里有哪些软中断? 66 | 67 | 在 Linux 系统里,我们可以通过查看 `/proc/softirqs` 的 内容来知晓「软中断」的运行情况,以及 `/proc/interrupts` 的 内容来知晓「硬中断」的运行情况。 68 | 69 | 接下来,就来简单的解析下 `/proc/softirqs` 文件的内容,在我服务器上查看到的文件内容如下: 70 | 71 | 72 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/操作系统/软中断/softirqs.png) 73 | 74 | 你可以看到,每一个 CPU 都有自己对应的不同类型软中断的**累计运行次数**,有 3 点需要注意下。 75 | 76 | 第一点,要注意第一列的内容,它是代表着软中断的类型,在我的系统里,软中断包括了 10 个类型,分别对应不同的工作类型,比如 `NET_RX` 表示网络接收中断,`NET_TX` 表示网络发送中断、`TIMER` 表示定时中断、`RCU` 表示 RCU 锁中断、`SCHED` 表示内核调度中断。 77 | 78 | 79 | 第二点,要注意同一种类型的软中断在不同 CPU 的分布情况,正常情况下,同一种中断在不同 CPU 上的累计次数相差不多,比如我的系统里,`NET_RX` 在 CPU0、CPU1、CPU2、CPU3 上的中断次数基本是同一个数量级,相差不多。 80 | 81 | 第三点,这些数值是系统运行以来的累计中断次数,数值的大小没什么参考意义,但是系统的**中断次数的变化速率**才是我们要关注的,我们可以使用 `watch -d cat /proc/softirqs` 命令查看中断次数的变化速率。 82 | 83 | 84 | 前面提到过,软中断是以内核线程的方式执行的,我们可以用 `ps` 命令可以查看到,下面这个就是在我的服务器上查到软中断内核线程的结果: 85 | 86 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/操作系统/软中断/ksoftirqd.png) 87 | 88 | 可以发现,内核线程的名字外面都有有中括号,这说明 ps 无法获取它们的命令行参数,所以一般来说,名字在中括号里的都可以认为是内核线程。 89 | 90 | 而且,你可以看到有 4 个 `ksoftirqd` 内核线程,这是因为我这台服务器的 CPU 是 4 核心的,每个 CPU 核心都对应着一个内核线程。 91 | 92 | --- 93 | 94 | ## 如何定位软中断 CPU 使用率过高的问题? 95 | 96 | 要想知道当前的系统的软中断情况,我们可以使用 `top` 命令查看,下面是一台服务器上的 top 的数据: 97 | 98 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/操作系统/软中断/top_si.png) 99 | 100 | 上图中的黄色部分 `si`,就是 CPU 在软中断上的使用率,而且可以发现,每个 CPU 使用率都不高,两个 CPU 的使用率虽然只有 3% 和 4% 左右,但是都是用在软中断上了。 101 | 102 | 另外,也可以看到 CPU 使用率最高的进程也是软中断 `ksoftirqd`,因此可以认为此时系统的开销主要来源于软中断。 103 | 104 | 如果要知道是哪种软中断类型导致的,我们可以使用 `watch -d cat /proc/softirqs` 命令查看每个软中断类型的中断次数的变化速率。 105 | 106 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/操作系统/软中断/watch.png) 107 | 108 | 一般对于网络 I/O 比较高的 Web 服务器,`NET_RX` 网络接收中断的变化速率相比其他中断类型快很多。 109 | 110 | 如果发现 `NET_RX` 网络接收中断次数的变化速率过快,接下来就可以使用 `sar -n DEV` 查看网卡的网络包接收速率情况,然后分析是哪个网卡有大量的网络包进来。 111 | 112 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/操作系统/软中断/sar_dev.png) 113 | 114 | 115 | 接着,在通过 `tcpdump` 抓包,分析这些包的来源,如果是非法的地址,可以考虑加防火墙,如果是正常流量,则要考虑硬件升级等。 116 | 117 | --- 118 | 119 | ## 总结 120 | 121 | 为了避免由于中断处理程序执行时间过长,而影响正常进程的调度,Linux 将中断处理程序分为上半部和下半部: 122 | 123 | - 上半部,对应硬中断,由硬件触发中断,用来快速处理中断; 124 | - 下半部,对应软中断,由内核触发中断,用来异步处理上半部未完成的工作; 125 | 126 | Linux 中的软中断包括网络收发、定时、调度、RCU 锁等各种类型,可以通过查看 /proc/softirqs 来观察软中断的累计中断次数情况,如果要实时查看中断次数的变化率,可以使用 watch -d cat /proc/softirqs 命令。 127 | 128 | 每一个 CPU 都有各自的软中断内核线程,我们还可以用 ps 命令来查看内核线程,一般名字在中括号里面到,都认为是内核线程。 129 | 130 | 如果在 top 命令发现,CPU 在软中断上的使用率比较高,而且 CPU 使用率最高的进程也是软中断 ksoftirqd 的时候,这种一般可以认为系统的开销被软中断占据了。 131 | 132 | 这时我们就可以分析是哪种软中断类型导致的,一般来说都是因为网络接收软中断导致的,如果是的话,可以用 sar 命令查看是哪个网卡的有大量的网络包接收,再用 tcpdump 抓网络包,做进一步分析该网络包的源头是不是非法地址,如果是就需要考虑防火墙增加规则,如果不是,则考虑硬件升级等。 133 | 134 | --- 135 | 136 | ## 关注作者 137 | 138 | ***哈喽,我是小林,就爱图解计算机基础,如果觉得文章对你有帮助,欢迎微信搜索「小林 coding」,关注后,回复「网络」再送你图解网络 PDF*** 139 | 140 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/其他/公众号介绍.png) -------------------------------------------------------------------------------- /os/2_os_structure/linux_vs_windows.md: -------------------------------------------------------------------------------- 1 | # 3.1 Linux 内核 vs Windows 内核 2 | 3 | Windows 和 Linux 可以说是我们比较常见的两款操作系统的。 4 | 5 | Windows 基本占领了电脑时代的市场,商业上取得了很大成就,但是它并不开源,所以要想接触源码得加入 Windows 的开发团队中。 6 | 7 | 对于服务器使用的操作系统基本上都是 Linux,而且内核源码也是开源的,任何人都可以下载,并增加自己的改动或功能,Linux 最大的魅力在于,全世界有非常多的技术大佬为它贡献代码。 8 | 9 | 这两个操作系统各有千秋,不分伯仲。 10 | 11 | 操作系统核心的东西就是内核,这次我们就来看看,**Linux 内核和 Windows 内核有什么区别?** 12 | 13 | --- 14 | 15 | ## 内核 16 | 17 | 什么是内核呢? 18 | 19 | 计算机是由各种外部硬件设备组成的,比如内存、cpu、硬盘等,如果每个应用都要和这些硬件设备对接通信协议,那这样太累了,所以这个中间人就由内核来负责,**让内核作为应用连接硬件设备的桥梁**,应用程序只需关心与内核交互,不用关心硬件的细节。 20 | 21 | ![内核](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/操作系统/内核/Kernel_Layout.png) 22 | 23 | 24 | 内核有哪些能力呢? 25 | 26 | 现代操作系统,内核一般会提供 4 个基本能力: 27 | 28 | - 管理进程、线程,决定哪个进程、线程使用 CPU,也就是进程调度的能力; 29 | - 管理内存,决定内存的分配和回收,也就是内存管理的能力; 30 | - 管理硬件设备,为进程与硬件设备之间提供通信能力,也就是硬件通信能力; 31 | - 提供系统调用,如果应用程序要运行更高权限运行的服务,那么就需要有系统调用,它是用户程序与操作系统之间的接口。 32 | 33 | 34 | 内核是怎么工作的? 35 | 36 | 内核具有很高的权限,可以控制 cpu、内存、硬盘等硬件,而应用程序具有的权限很小,因此大多数操作系统,把内存分成了两个区域: 37 | 38 | - 内核空间,这个内存空间只有内核程序可以访问; 39 | - 用户空间,这个内存空间专门给应用程序使用; 40 | 41 | 用户空间的代码只能访问一个局部的内存空间,而内核空间的代码可以访问所有内存空间。因此,当程序使用用户空间时,我们常说该程序在**用户态**执行,而当程序使内核空间时,程序则在**内核态**执行。 42 | 43 | 44 | 应用程序如果需要进入内核空间,就需要通过系统调用,下面来看看系统调用的过程: 45 | 46 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/操作系统/内核/systemcall.png) 47 | 48 | 内核程序执行在内核态,用户程序执行在用户态。当应用程序使用系统调用时,会产生一个中断。发生中断后,CPU 会中断当前在执行的用户程序,转而跳转到中断处理程序,也就是开始执行内核程序。内核处理完后,主动触发中断,把 CPU 执行权限交回给用户程序,回到用户态继续工作。 49 | 50 | 51 | --- 52 | 53 | ## Linux 的设计 54 | 55 | Linux 的开山始祖是来自一位名叫 Linus Torvalds 的芬兰小伙子,他在 1991 年用 C 语言写出了第一版的 Linux 操作系统,那年他 22 岁。 56 | 57 | 完成第一版 Linux 后,Linux Torvalds 就在网络上发布了 Linux 内核的源代码,每个人都可以免费下载和使用。 58 | 59 | 60 | Linux 内核设计的理念主要有这几个点: 61 | 62 | - *MultiTask*,多任务 63 | - *SMP*,对称多处理 64 | - *ELF*,可执行文件链接格式 65 | - *Monolithic Kernel*,宏内核 66 | 67 | #### MultiTask 68 | 69 | MultiTask 的意思是**多任务**,代表着 Linux 是一个多任务的操作系统。 70 | 71 | 多任务意味着可以有多个任务同时执行,这里的「同时」可以是并发或并行: 72 | 73 | - 对于单核 CPU 时,可以让每个任务执行一小段时间,时间到就切换另外一个任务,从宏观角度看,一段时间内执行了多个任务,这被称为并发。 74 | - 对于多核 CPU 时,多个任务可以同时被不同核心的 CPU 同时执行,这被称为并行。 75 | 76 | ### SMP 77 | 78 | 79 | SMP 的意思是**对称多处理**,代表着每个 CPU 的地位是相等的,对资源的使用权限也是相同的,多个 CPU 共享同一个内存,每个 CPU 都可以访问完整的内存和硬件资源。 80 | 81 | 这个特点决定了 Linux 操作系统不会有某个 CPU 单独服务应用程序或内核程序,而是每个程序都可以被分配到任意一个 CPU 上被执行。 82 | 83 | ### ELF 84 | 85 | ELF 的意思是**可执行文件链接格式**,它是 Linux 操作系统中可执行文件的存储格式,你可以从下图看到它的结构: 86 | 87 | 88 | ![ELF 文件格式](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/操作系统/内核/Elf.png) 89 | 90 | 91 | ELF 把文件分成了一个个分段,每一个段都有自己的作用,具体每个段的作用这里我就不详细说明了,感兴趣的同学可以去看《程序员的自我修养——链接、装载和库》这本书。 92 | 93 | 另外,ELF 文件有两种索引,Program header table 中记录了「运行时」所需的段,而 Section header table 记录了二进制文件中各个「段的首地址」。 94 | 95 | 那 ELF 文件怎么生成的呢? 96 | 97 | 我们编写的代码,首先通过「编译器」编译成汇编代码,接着通过「汇编器」变成目标代码,也就是目标文件,最后通过「链接器」把多个目标文件以及调用的各种函数库链接起来,形成一个可执行文件,也就是 ELF 文件。 98 | 99 | 那 ELF 文件是怎么被执行的呢? 100 | 101 | 执行 ELF 文件的时候,会通过「装载器」把 ELF 文件装载到内存里,CPU 读取内存中的指令和数据,于是程序就被执行起来了。 102 | 103 | 104 | ### Monolithic Kernel 105 | 106 | Monolithic Kernel 的意思是**宏内核**,Linux 内核架构就是宏内核,意味着 Linux 的内核是一个完整的可执行程序,且拥有最高的权限。 107 | 108 | 109 | 宏内核的特征是系统内核的所有模块,比如进程调度、内存管理、文件系统、设备驱动等,都运行在内核态。 110 | 111 | 不过,Linux 也实现了动态加载内核模块的功能,例如大部分设备驱动是以可加载模块的形式存在的,与内核其他模块解耦,让驱动开发和驱动加载更为方便、灵活。 112 | 113 | 114 | ![分别为宏内核、微内核、混合内核的操作系统结构](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/操作系统/内核/OS-structure2.png) 115 | 116 | 与宏内核相反的是**微内核**,微内核架构的内核只保留最基本的能力,比如进程调度、虚拟机内存、中断等,把一些应用放到了用户空间,比如驱动程序、文件系统等。这样服务与服务之间是隔离的,单个服务出现故障或者完全攻击,也不会导致整个操作系统挂掉,提高了操作系统的稳定性和可靠性。 117 | 118 | 微内核内核功能少,可移植性高,相比宏内核有一点不好的地方在于,由于驱动程序不在内核中,而且驱动程序一般会频繁调用底层能力的,于是驱动和硬件设备交互就需要频繁切换到内核态,这样会带来性能损耗。华为的鸿蒙操作系统的内核架构就是微内核。 119 | 120 | 121 | 还有一种内核叫**混合类型内核**,它的架构有点像微内核,内核里面会有一个最小版本的内核,然后其他模块会在这个基础上搭建,然后实现的时候会跟宏内核类似,也就是把整个内核做成一个完整的程序,大部分服务都在内核中,这就像是宏内核的方式包裹着一个微内核。 122 | 123 | --- 124 | 125 | ## Windows 设计 126 | 127 | 128 | 当今 Windows 7、Windows 10 使用的内核叫 Windows NT,NT 全称叫 New Technology。 129 | 130 | 下图是 Windows NT 的结构图片: 131 | 132 | ![Windows NT 的结构](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/操作系统/内核/windowNT.png) 133 | 134 | Windows 和 Linux 一样,同样支持 MutiTask 和 SMP,但不同的是,**Window 的内核设计是混合型内核**,在上图你可以看到内核中有一个 *MicroKernel* 模块,这个就是最小版本的内核,而整个内核实现是一个完整的程序,含有非常多模块。 135 | 136 | Windows 的可执行文件的格式与 Linux 也不同,所以这两个系统的可执行文件是不可以在对方上运行的。 137 | 138 | Windows 的可执行文件格式叫 PE,称为**可移植执行文件**,扩展名通常是`.exe`、`.dll`、`.sys`等。 139 | 140 | PE 的结构你可以从下图中看到,它与 ELF 结构有一点相似。 141 | 142 | ![PE 文件结构](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/操作系统/内核/pe.png) 143 | 144 | --- 145 | 146 | ## 总结 147 | 148 | 对于内核的架构一般有这三种类型: 149 | 150 | - 宏内核,包含多个模块,整个内核像一个完整的程序; 151 | - 微内核,有一个最小版本的内核,一些模块和服务则由用户态管理; 152 | - 混合内核,是宏内核和微内核的结合体,内核中抽象出了微内核的概念,也就是内核中会有一个小型的内核,其他模块就在这个基础上搭建,整个内核是个完整的程序; 153 | 154 | Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用了混合内核。 155 | 156 | 这两个操作系统的可执行文件格式也不一样,Linux 可执行文件格式叫作 ELF,Windows 可执行文件格式叫作 PE。 157 | 158 | --- 159 | 160 | ## 关注作者 161 | 162 | ***哈喽,我是小林,就爱图解计算机基础,如果觉得文章对你有帮助,欢迎微信搜索「小林 coding」,关注后,回复「网络」再送你图解网络 PDF*** 163 | 164 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/其他/公众号介绍.png) 165 | -------------------------------------------------------------------------------- /os/4_process/create_thread_max.md: -------------------------------------------------------------------------------- 1 | # 5.6 一个进程最多可以创建多少个线程? 2 | 3 | 大家好,我是小林。 4 | 5 | 昨天有位读者问了我这么个问题: 6 | 7 | ![](https://img-blog.csdnimg.cn/20210715092002563.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 8 | ![](https://img-blog.csdnimg.cn/20210715092015507.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 9 | 10 | 11 | 大致意思就是,他看了一个面经,说虚拟内存是 2G 大小,然后他看了我的图解系统 PDF 里说虚拟内存是 4G,然后他就懵逼了。 12 | 13 | 其实他看这个面经很有问题,没有说明是什么操作系统,以及是多少位操作系统。 14 | 15 | 因为不同的操作系统和不同位数的操作系统,虚拟内存可能是不一样多。 16 | 17 | Windows 系统我不了解,我就说说 Linux 系统。 18 | 19 | 20 | 在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址 空间的范围也不同。比如最常⻅的 32 位和 64 位系统,如下所示: 21 | 22 | ![](https://img-blog.csdnimg.cn/20210715092026648.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 23 | 24 | 25 | 26 | 通过这里可以看出: 27 | - 32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间; 28 | - 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中 29 | 间部分是未定义的。 30 | 31 | --- 32 | 33 | 接着,来看看读者那个面经题目:**一个进程最多可以创建多少个线程?** 34 | 35 | 这个问题跟两个东西有关系: 36 | - **进程的虚拟内存空间上限**,因为创建一个线程,操作系统需要为其分配一个栈空间,如果线程数量越多,所需的栈空间就要越大,那么虚拟内存就会占用的越多。 37 | - **系统参数限制**,虽然 Linux 并没有内核参数来控制单个进程创建的最大线程个数,但是有系统级别的参数来控制整个系统的最大线程个数。 38 | 39 | 40 | 我们先看看,在进程里创建一个线程需要消耗多少虚拟内存大小? 41 | 42 | 我们可以执行 ulimit -a 这条命令,查看进程创建线程时默认分配的栈空间大小,比如我这台服务器默认分配给线程的栈空间大小为 8M。 43 | 44 | ![](https://img-blog.csdnimg.cn/20210715092041211.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 45 | 46 | 47 | 48 | 在前面我们知道,在 32 位 Linux 系统里,一个进程的虚拟空间是 4G,内核分走了 1G,**留给用户用的只有 3G**。 49 | 50 | 那么假设创建一个线程需要占用 10M 虚拟内存,总共有 3G 虚拟内存可以使用。于是我们可以算出,最多可以创建差不多 300 个(3G/10M)左右的线程。 51 | 52 | 53 | 如果你想自己做个实验,你可以找台 32 位的 Linux 系统运行下面这个代码: 54 | 55 | ![](https://img-blog.csdnimg.cn/20210715092052531.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 56 | 57 | 58 | 由于我手上没有 32 位的系统,我这里贴一个网上别人做的测试结果: 59 | 60 | ![](https://img-blog.csdnimg.cn/202107150921005.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 61 | 62 | 63 | 64 | 65 | 如果想使得进程创建上千个线程,那么我们可以调整创建线程时分配的栈空间大小,比如调整为 512k: 66 | 67 | ```plain 68 | $ ulimit -s 512 69 | ``` 70 | 71 | ---- 72 | 73 | 说完 32 位系统的情况,我们来看看 64 位系统里,一个进程能创建多少线程呢? 74 | 75 | 我的测试服务器的配置: 76 | - 64 位系统; 77 | - 2G 物理内存; 78 | - 单核 CPU。 79 | 80 | 64 位系统意味着用户空间的虚拟内存最大值是 128T,这个数值是很大的,如果按创建一个线程需占用 10M 栈空间的情况来算,那么理论上可以创建 128T/10M 个线程,也就是 1000 多万个线程,有点魔幻! 81 | 82 | 所以按 64 位系统的虚拟内存大小,理论上可以创建无数个线程。 83 | 84 | 事实上,肯定创建不了那么多线程,除了虚拟内存的限制,还有系统的限制。 85 | 86 | 比如下面这三个内核参数的大小,都会影响创建线程的上限: 87 | - ***/proc/sys/kernel/threads-max***,表示系统支持的最大线程数,默认值是 `14553`; 88 | - ***/proc/sys/kernel/pid_max***,表示系统全局的 PID 号数值的限制,每一个进程或线程都有 ID,ID 的值超过这个数,进程或线程就会创建失败,默认值是 `32768`; 89 | - ***/proc/sys/vm/max_map_count***,表示限制一个进程可以拥有的 VMA(虚拟内存区域) 的数量,具体什么意思我也没搞清楚,反正如果它的值很小,也会导致创建线程失败,默认值是 `65530`。 90 | 91 | 92 | 93 | 那接下针对我的测试服务器的配置,看下一个进程最多能创建多少个线程呢? 94 | 95 | 我在这台服务器跑了前面的程序,其结果如下: 96 | 97 | ![](https://img-blog.csdnimg.cn/20210715092109740.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 98 | 99 | 100 | 101 | 可以看到,创建了 14374 个线程后,就无法在创建了,而且报错是因为资源的限制。 102 | 103 | 前面我提到的 `threads-max` 内核参数,它是限制系统里最大线程数,默认值是 14553。 104 | 105 | 106 | 107 | 我们可以运行那个测试线程数的程序后,看下当前系统的线程数是多少,可以通过 `top -H` 查看。 108 | 109 | ![](https://img-blog.csdnimg.cn/20210715092125376.png) 110 | 111 | 112 | 左上角的 Threads 的数量显示是 14553,与 `threads-max` 内核参数的值相同,所以我们可以认为是因为这个参数导致无法继续创建线程。 113 | 114 | 那么,我们可以把 threads-max 参数设置成 `99999`: 115 | 116 | 117 | ```plain 118 | echo 99999 > /proc/sys/kernel/threads-max 119 | ``` 120 | 121 | 设置完 threads-max 参数后,我们重新跑测试线程数的程序,运行后结果如下图: 122 | 123 | ![](https://img-blog.csdnimg.cn/20210715092138115.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 124 | 125 | 126 | 127 | 可以看到,当进程创建了 32326 个线程后,就无法继续创建里,且报错是无法继续申请内存。 128 | 129 | 此时的上限个数很接近 `pid_max` 内核参数的默认值(32768),那么我们可以尝试将这个参数设置为 99999: 130 | 131 | 132 | ```plain 133 | echo 99999 > /proc/sys/kernel/pid_max 134 | ``` 135 | 136 | 设置完 pid_max 参数后,继续跑测试线程数的程序,运行后结果创建线程的个数还是一样卡在了 32768 了。 137 | 138 | 当时我也挺疑惑的,明明 pid_max 已经调整大后,为什么线程个数还是上不去呢? 139 | 140 | 后面经过查阅资料发现,`max_map_count` 这个内核参数也是需要调大的,但是它的数值与最大线程数之间有什么关系,我也不太明白,只是知道它的值是会限制创建线程个数的上限。 141 | 142 | 然后,我把 max_map_count 内核参数也设置成后 99999: 143 | 144 | ```plain 145 | echo 99999 > /proc/sys/kernel/max_map_count 146 | ``` 147 | 148 | 继续跑测试线程数的程序,结果如下图: 149 | 150 | ![](https://img-blog.csdnimg.cn/20210715092151214.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 151 | 152 | 153 | 当创建差不多 5 万个线程后,我的服务器就卡住不动了,CPU 都已经被占满了,毕竟这个是单核 CPU,所以现在是 CPU 的瓶颈了。 154 | 155 | 我只有这台服务器,如果你们有性能更强的服务器来测试的话,有兴趣的小伙伴可以去测试下。 156 | 157 | 接下来,我们换个思路测试下,把创建线程时分配的栈空间调大,比如调大为 1000M,在大就会创建线程失败。 158 | 159 | ```plain 160 | ulimit -s 1024000 161 | ``` 162 | 163 | 设置完后,跑测试线程的程序,其结果如下: 164 | 165 | ![](https://img-blog.csdnimg.cn/20210715092207662.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70) 166 | 167 | 168 | 总共创建了 26390 个线程,然后就无法继续创建了,而且该进程的虚拟内存空间已经高达 25T,要知道这台服务器的物理内存才 2G。 169 | 170 | 为什么物理内存只有 2G,进程的虚拟内存却可以使用 25T 呢? 171 | 172 | 因为虚拟内存并不是全部都映射到物理内存的,程序是有局部性的特性,也就是某一个时间只会执行部分代码,所以只需要映射这部分程序就好。 173 | 174 | 你可以从上面那个 top 的截图看到,虽然进程虚拟空间很大,但是物理内存(RES)只有使用了 400 多 M。 175 | 176 | 177 | 好了,简单总结下: 178 | - 32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。 179 | - 64 位系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。 180 | 181 | -------------------------------------------------------------------------------- /os/4_process/pessim_and_optimi_lock.md: -------------------------------------------------------------------------------- 1 | # 5.5 什么是悲观锁、乐观锁? 2 | 3 | 生活中用到的锁,用途都比较简单粗暴,上锁基本是为了防止外人进来、电动车被偷等等。 4 | 5 | 但生活中也不是没有 BUG 的,比如加锁的电动车在「广西 - 窃·格瓦拉」面前,锁就是形同虚设,只要他愿意,他就可以轻轻松松地把你电动车给「顺走」,不然打工怎么会是他这辈子不可能的事情呢?牛逼之人,必有牛逼之处。 6 | 7 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/其他/窃格瓦拉.jpg) 8 | 9 | 10 | 那在编程世界里,「锁」更是五花八门,多种多样,每种锁的加锁开销以及应用场景也可能会不同。 11 | 12 | 如何用好锁,也是程序员的基本素养之一了。 13 | 14 | 高并发的场景下,如果选对了合适的锁,则会大大提高系统的性能,否则性能会降低。 15 | 16 | 所以,知道各种锁的开销,以及应用场景是很有必要的。 17 | 18 | 接下来,就谈一谈常见的这几种锁: 19 | 20 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/操作系统/锁/锁之提供.png) 21 | 22 | 23 | --- 24 | 25 | 多线程访问共享资源的时候,避免不了资源竞争而导致数据错乱的问题,所以我们通常为了解决这一问题,都会在访问共享资源之前加锁。 26 | 27 | 最常用的就是互斥锁,当然还有很多种不同的锁,比如自旋锁、读写锁、乐观锁等,不同种类的锁自然适用于不同的场景。 28 | 29 | 如果选择了错误的锁,那么在一些高并发的场景下,可能会降低系统的性能,这样用户体验就会非常差了。 30 | 31 | 所以,为了选择合适的锁,我们不仅需要清楚知道加锁的成本开销有多大,还需要分析业务场景中访问的共享资源的方式,再来还要考虑并发访问共享资源时的冲突概率。 32 | 33 | 对症下药,才能减少锁对高并发性能的影响。 34 | 35 | 那接下来,针对不同的应用场景,谈一谈「**互斥锁、自旋锁、读写锁、乐观锁、悲观锁**」的选择和使用。 36 | 37 | ## 互斥锁与自旋锁 38 | 39 | 最底层的两种就是会「互斥锁和自旋锁」,有很多高级的锁都是基于它们实现的,你可以认为它们是各种锁的地基,所以我们必须清楚它俩之间的区别和应用。 40 | 41 | 加锁的目的就是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。 42 | 43 | 当已经有一个线程加锁后,其他线程加锁则就会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的: 44 | 45 | - **互斥锁**加锁失败后,线程会**释放 CPU** ,给其他线程; 46 | - **自旋锁**加锁失败后,线程会**忙等待**,直到它拿到锁; 47 | 48 | 互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,**既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞**。 49 | 50 | **对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的**。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。如下图: 51 | 52 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/操作系统/锁/互斥锁工作流程.png) 53 | 54 | 所以,互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮我们切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本。 55 | 56 | 那这个开销成本是什么呢?会有**两次线程上下文切换的成本**: 57 | 58 | - 当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行; 59 | - 接着,当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运行。 60 | 61 | 线程的上下文切换的是什么?当两个线程是属于同一个进程,**因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。** 62 | 63 | 上下文切换的耗时有大佬统计过,大概在几十纳秒到几微秒之间,如果你锁住的代码执行时间比较短,那可能上下文切换的时间都比你锁住的代码执行时间还要长。 64 | 65 | 所以,**如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。** 66 | 67 | 自旋锁是通过 CPU 提供的 `CAS` 函数(*Compare And Swap*),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。 68 | 69 | 一般加锁的过程,包含两个步骤: 70 | 71 | - 第一步,查看锁的状态,如果锁是空闲的,则执行第二步; 72 | - 第二步,将锁设置为当前线程持有; 73 | 74 | CAS 函数就把这两个步骤合并成一条硬件级指令,形成**原子指令**,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。 75 | 76 | 比如,设锁为变量 lock,整数 0 表示锁是空闲状态,整数 pid 表示线程 ID,那么 CAS(lock, 0, pid) 就表示自旋锁的加锁操作,CAS(lock, pid, 0) 则表示解锁操作。 77 | 78 | 使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会「忙等待」,直到它拿到锁。这里的「忙等待」可以用 `while` 循环等待实现,不过最好是使用 CPU 提供的 `PAUSE` 指令来实现「忙等待」,因为可以减少循环等待时的耗电量。 79 | 80 | 自旋锁是最简单的一种锁,一直自旋,利用 CPU 周期,直到锁可用。**需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。** 81 | 82 | 自旋锁开销少,在多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和被锁住的代码执行的时间是成「正比」的关系,我们需要清楚的知道这一点。 83 | 84 | 自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同:**当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对**。 85 | 86 | 它俩是锁的最基本处理方式,更高级的锁都会选择其中一个来实现,比如读写锁既可以选择互斥锁实现,也可以基于自旋锁实现。 87 | 88 | --- 89 | 90 | ## 读写锁 91 | 92 | 读写锁从字面意思我们也可以知道,它由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。 93 | 94 | 所以,**读写锁适用于能明确区分读操作和写操作的场景**。 95 | 96 | 读写锁的工作原理是: 97 | 98 | - 当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。 99 | - 但是,一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。 100 | 101 | 所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。 102 | 103 | 知道了读写锁的工作原理后,我们可以发现,**读写锁在读多写少的场景,能发挥出优势**。 104 | 105 | 另外,根据实现的不同,读写锁可以分为「读优先锁」和「写优先锁」。 106 | 107 | 读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取写锁。如下图: 108 | 109 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/操作系统/锁/读优先锁工作流程.png) 110 | 111 | 112 | 而「写优先锁」是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取写锁。如下图: 113 | 114 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/操作系统/锁/写优先锁工作流程.png) 115 | 116 | 117 | 读优先锁对于读线程并发性更好,但也不是没有问题。我们试想一下,如果一直有读线程获取读锁,那么写线程将永远获取不到写锁,这就造成了写线程「饥饿」的现象。 118 | 119 | 写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被「饿死」。 120 | 121 | 既然不管优先读锁还是写锁,对方可能会出现饿死问题,那么我们就不偏袒任何一方,搞个「公平读写锁」。 122 | 123 | **公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。** 124 | 125 | 互斥锁和自旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁其中的一个进行实现。 126 | 127 | --- 128 | 129 | ## 乐观锁与悲观锁 130 | 131 | 前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。 132 | 133 | 悲观锁做事比较悲观,它认为**多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁**。 134 | 135 | 那相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。 136 | 137 | 乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:**先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作**。 138 | 139 | 放弃后如何重试,这跟业务场景息息相关,虽然重试的成本很高,但是冲突的概率足够低的话,还是可以接受的。 140 | 141 | 可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现**乐观锁全程并没有加锁,所以它也叫无锁编程**。 142 | 143 | 这里举一个场景例子:在线文档。 144 | 145 | 我们都知道在线文档可以同时多人编辑的,如果使用了悲观锁,那么只要有一个用户正在编辑文档,此时其他用户就无法打开相同的文档了,这用户体验当然不好了。 146 | 147 | 那实现多人同时编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。 148 | 149 | 怎么样才算发生冲突?这里举个例子,比如用户 A 先在浏览器编辑文档,之后用户 B 在浏览器也打开了相同的文档进行编辑,但是用户 B 比用户 A 提交早,这一过程用户 A 是不知道的,当 A 提交修改完的内容时,那么 A 和 B 之间并行修改的地方就会发生冲突。 150 | 151 | 服务端要怎么验证是否冲突了呢?通常方案如下: 152 | 153 | - 由于发生冲突的概率比较低,所以先让用户编辑文档,但是浏览器在下载文档时会记录下服务端返回的文档版本号; 154 | - 当用户提交修改时,发给服务端的请求会带上原始文档版本号,服务器收到后将它与当前版本号进行比较,如果版本号不一致则提交失败,如果版本号一致则修改成功,然后服务端版本号更新到最新的版本号。 155 | 156 | 实际上,我们常见的 SVN 和 Git 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。 157 | 158 | 乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以**只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。** 159 | 160 | --- 161 | 162 | ## 总结 163 | 164 | 开发过程中,最常见的就是互斥锁的了,互斥锁加锁失败时,会用「线程切换」来应对,当加锁失败的线程再次加锁成功后的这一过程,会有两次线程上下文切换的成本,性能损耗比较大。 165 | 166 | 如果我们明确知道被锁住的代码的执行时间很短,那我们应该选择开销比较小的自旋锁,因为自旋锁加锁失败时,并不会主动产生线程切换,而是一直忙等待,直到获取到锁,那么如果被锁住的代码执行时间很短,那这个忙等待的时间相对应也很短。 167 | 168 | 169 | 如果能区分读操作和写操作的场景,那读写锁就更合适了,它允许多个读线程可以同时持有读锁,提高了读的并发性。根据偏袒读方还是写方,可以分为读优先锁和写优先锁,读优先锁并发性很强,但是写线程会被饿死,而写优先锁会优先服务写线程,读线程也可能会被饿死,那为了避免饥饿的问题,于是就有了公平读写锁,它是用队列把请求锁的线程排队,并保证先入先出的原则来对线程加锁,这样便保证了某种线程不会被饿死,通用性也更好点。 170 | 171 | 互斥锁和自旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁其中的一个进行实现。 172 | 173 | 另外,互斥锁、自旋锁、读写锁都属于悲观锁,悲观锁认为并发访问共享资源时,冲突概率可能非常高,所以在访问共享资源前,都需要先加锁。 174 | 175 | 相反的,如果并发访问共享资源时,冲突概率非常低的话,就可以使用乐观锁,它的工作方式是,在访问共享资源时,不用先加锁,修改完共享资源后,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。 176 | 177 | 但是,一旦冲突概率上升,就不适合使用乐观锁了,因为它解决冲突的重试成本非常高。 178 | 179 | 不管使用的哪种锁,我们的加锁的代码范围应该尽可能的小,也就是加锁的粒度要小,这样执行速度会比较快。再来,使用上了合适的锁,就会快上加快了。 180 | 181 | ## 读者问答 182 | 183 | > CAS 不是乐观锁吗,为什么基于 CAS 实现的自旋锁是悲观锁? 184 | 185 | 乐观锁是先修改同步资源,再验证有没有发生冲突。 186 | 187 | 悲观锁是修改共享数据前,都要先加锁,防止竞争。 188 | 189 | CAS 是乐观锁没错,但是 CAS 和自旋锁不同之处,自旋锁基于 CAS 加了 while 或者睡眠 CPU 的操作而产生自旋的效果,加锁失败会忙等待直到拿到锁,自旋锁是要需要事先拿到锁才能修改数据的,所以算悲观锁。 190 | 191 | --- 192 | 193 | ## 关注作者 194 | 195 | 这周末忙里偷闲了下,看了三部电影,简单说一下感受。 196 | 197 | 首先看了「利刃出鞘」,这部电影是悬疑类型,也是豆瓣高分电影,电影虽然没有什么大场面,但是单纯靠缜密的剧情铺设,全程无尿点,结尾也各种翻转,如果喜欢悬疑类电影朋友,不妨抽个时间看看。 198 | 199 | 再来,看了「花木兰」,这电影我特喵无法可说,烂片中的战斗鸡,演员都是中国人却全在说英文(导演是美国迪士尼的),这种感觉就很奇怪很别扭,好比你看西游记、水浒传英文版那样的别扭。别扭也就算了,关键剧情平淡无奇,各种无厘头的地方,反正看完之后,我非常后悔把我生命中非常珍贵的 2 个小时献给了它,如果能重来,我选择用这 2 小时睡觉。 200 | 201 | 最后,当然看了「信条」,诺兰用巨资拍摄出来的电影,花钱买飞机来撞,画面非常震撼,可以说非常有诚意了。诺兰钟爱时间的概念,这次则以时间倒流方式来呈现,非常的烧脑,反正我看完后脑袋懵懵的,我就是要这种感觉,嘻嘻。 202 | 203 | 204 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 205 | 206 | **大家好,我是小林,一个专为大家图解的工具人,如果觉得文章对你有帮助,欢迎分享给你的朋友,我们下次见!** 207 | -------------------------------------------------------------------------------- /os/8_network_system/hash.md: -------------------------------------------------------------------------------- 1 | # 9.4 什么是一致性哈希? 2 | 3 | 大家好,我是小林。 4 | 5 | 在逛牛客网的面经的时候,发现有位同学在面微信的时候,被问到这个问题: 6 | 7 | ![](https://img-blog.csdnimg.cn/img_convert/2ad888cd9ca79d8d68fbd7ff29a6e088.png) 8 | 9 | 第一个问题就是:**一致性哈希是什么,使用场景,解决了什么问题?** 10 | 11 | 这个问题还挺有意思的,所以今天就来聊聊这个。 12 | 13 | 发车! 14 | 15 | ![](https://img-blog.csdnimg.cn/img_convert/7de125e1b754aa50132e1fa385ad5c0a.png) 16 | 17 | ## 如何分配请求? 18 | 19 | 大多数网站背后肯定不是只有一台服务器提供服务,因为单机的并发量和数据量都是有限的,所以都会用多台服务器构成集群来对外提供服务。 20 | 21 | 但是问题来了,现在有那么多个节点(后面统称服务器为节点,因为少一个字),要如何分配客户端的请求呢? 22 | 23 | ![](https://img-blog.csdnimg.cn/img_convert/b752a4f8dcaab8ed4d941ebcc6f606c5.png) 24 | 25 | 其实这个问题就是「负载均衡问题」。解决负载均衡问题的算法很多,不同的负载均衡算法,对应的就是不同的分配策略,适应的业务场景也不同。 26 | 27 | 最简单的方式,引入一个中间的负载均衡层,让它将外界的请求「轮流」的转发给内部的集群。比如集群有 3 个节点,外界请求有 3 个,那么每个节点都会处理 1 个请求,达到了分配请求的目的。 28 | 29 | ![](https://img-blog.csdnimg.cn/img_convert/d3279ad754257977f98e702cb156e9cf.png) 30 | 31 | 考虑到每个节点的硬件配置有所区别,我们可以引入权重值,将硬件配置更好的节点的权重值设高,然后根据各个节点的权重值,按照一定比重分配在不同的节点上,让硬件配置更好的节点承担更多的请求,这种算法叫做加权轮询。 32 | 33 | 加权轮询算法使用场景是建立在每个节点存储的数据都是相同的前提。所以,每次读数据的请求,访问任意一个节点都能得到结果。 34 | 35 | 但是,加权轮询算法是无法应对「分布式系统」的,因为分布式系统中,每个节点存储的数据是不同的。 36 | 37 | 当我们想提高系统的容量,就会将数据水平切分到不同的节点来存储,也就是将数据分布到了不同的节点。比如**一个分布式 KV(key-value)缓存系统,某个 key 应该到哪个或者哪些节点上获得,应该是确定的**,不是说任意访问一个节点都可以得到缓存结果的。 38 | 39 | 因此,我们要想一个能应对分布式系统的负载均衡算法。 40 | 41 | ## 使用哈希算法有什么问题? 42 | 43 | 有的同学可能很快就想到了:**哈希算法**。因为对同一个关键字进行哈希计算,每次计算都是相同的值,这样就可以将某个 key 确定到一个节点了,可以满足分布式系统的负载均衡需求。 44 | 45 | 哈希算法最简单的做法就是进行取模运算,比如分布式系统中有 3 个节点,基于 `hash(key) % 3` 公式对数据进行了映射。 46 | 47 | 如果客户端要获取指定 key 的数据,通过下面的公式可以定位节点: 48 | 49 | ```plain 50 | hash(key) % 3 51 | ``` 52 | 53 | 如果经过上面这个公式计算后得到的值是 0,就说明该 key 需要去第一个节点获取。 54 | 55 | 但是有一个很致命的问题,**如果节点数量发生了变化,也就是在对系统做扩容或者缩容时,必须迁移改变了映射关系的数据**,否则会出现查询不到数据的问题。 56 | 57 | 举个例子,假设我们有一个由 A、B、C 三个节点组成分布式 KV 缓存系统,基于计算公式 `hash(key) % 3` 将数据进行了映射,每个节点存储了不同的数据: 58 | 59 | ![](https://img-blog.csdnimg.cn/img_convert/025ddcaabece1f4b5823dfb1fb7340ef.png) 60 | 61 | 现在有 3 个查询 key 的请求,分别查询 key-01,key-02,key-03 的数据,这三个 key 分别经过 hash() 函数计算后的值为 hash( key-01) = 6、hash( key-02) = 7、hash(key-03) = 8,然后再对这些值进行取模运算。 62 | 63 | 通过这样的哈希算法,每个 key 都可以定位到对应的节点。 64 | 65 | ![](https://img-blog.csdnimg.cn/img_convert/ed14c96417e08b4f916e0cd23d12b7bd.png) 66 | 67 | 当 3 个节点不能满足业务需求了,这时我们增加了一个节点,节点的数量从 3 变化为 4,意味取模哈希函数中基数的变化,这样会导致**大部分映射关系改变**,如下图: 68 | 69 | ![](https://img-blog.csdnimg.cn/img_convert/392c54cfb9ec47f5191008aa1d27d6b5.png) 70 | 71 | 比如,之前的 hash(key-01) % `3` = 0,就变成了 hash(key-01) % `4` = 2,查询 key-01 数据时,寻址到了节点 C,而 key-01 的数据是存储在节点 A 上的,不是在节点 C,所以会查询不到数据。 72 | 73 | 同样的道理,如果我们对分布式系统进行缩容,比如移除一个节点,也会因为取模哈希函数中基数的变化,可能出现查询不到数据的问题。 74 | 75 | 要解决这个问题的办法,就需要我们进行**迁移数据**,比如节点的数量从 3 变化为 4 时,要基于新的计算公式 hash(key) % 4,重新对数据和节点做映射。 76 | 77 | 假设总数据条数为 M,哈希算法在面对节点数量变化时,**最坏情况下所有数据都需要迁移,所以它的数据迁移规模是 O(M)**,这样数据的迁移成本太高了。 78 | 79 | 所以,我们应该要重新想一个新的算法,来避免分布式系统在扩容或者缩容时,发生过多的数据迁移。 80 | 81 | ## 使用一致性哈希算法有什么问题? 82 | 83 | 一致性哈希算法就很好地解决了分布式系统在扩容或者缩容时,发生过多的数据迁移的问题。 84 | 85 | 一致哈希算法也用了取模运算,但与哈希算法不同的是,哈希算法是对节点的数量进行取模运算,而**一致哈希算法是对 2^32 进行取模运算,是一个固定的值**。 86 | 87 | 我们可以把一致哈希算法是对 2^32 进行取模运算的结果值组织成一个圆环,就像钟表一样,钟表的圆可以理解成由 60 个点组成的圆,而此处我们把这个圆想象成由 2^32 个点组成的圆,这个圆环被称为**哈希环**,如下图: 88 | 89 | ![](https://img-blog.csdnimg.cn/img_convert/0ea3960fef48d4cbaeb4bec4345301e7.png) 90 | 91 | 一致性哈希要进行两步哈希: 92 | 93 | - 第一步:对存储节点进行哈希计算,也就是对存储节点做哈希映射,比如根据节点的 IP 地址进行哈希; 94 | - 第二步:当对数据进行存储或访问时,对数据进行哈希映射; 95 | 96 | 所以,**一致性哈希是指将「存储节点」和「数据」都映射到一个首尾相连的哈希环上**。 97 | 98 | 问题来了,对「数据」进行哈希映射得到一个结果要怎么找到存储该数据的节点呢? 99 | 100 | 答案是,映射的结果值往**顺时针的方向的找到第一个节点**,就是存储该数据的节点。 101 | 102 | 举个例子,有 3 个节点经过哈希计算,映射到了如下图的位置: 103 | 104 | ![](https://img-blog.csdnimg.cn/img_convert/83d7f363643353c92d252e34f1d4f687.png) 105 | 106 | 接着,对要查询的 key-01 进行哈希计算,确定此 key-01 映射在哈希环的位置,然后从这个位置往顺时针的方向找到第一个节点,就是存储该 key-01 数据的节点。 107 | 108 | 比如,下图中的 key-01 映射的位置,往顺时针的方向找到第一个节点就是节点 A。 109 | 110 | ![](https://img-blog.csdnimg.cn/img_convert/30c2c70721c12f9c140358fbdc5f2282.png) 111 | 112 | 所以,当需要对指定 key 的值进行读写的时候,要通过下面 2 步进行寻址: 113 | 114 | - 首先,对 key 进行哈希计算,确定此 key 在环上的位置; 115 | - 然后,从这个位置沿着顺时针方向走,遇到的第一节点就是存储 key 的节点。 116 | 117 | 知道了一致哈希寻址的方式,我们来看看,如果增加一个节点或者减少一个节点会发生大量的数据迁移吗? 118 | 119 | 假设节点数量从 3 增加到了 4,新的节点 D 经过哈希计算后映射到了下图中的位置: 120 | 121 | ![](https://img-blog.csdnimg.cn/img_convert/f8909edef2f3949f8945bb99380baab3.png) 122 | 123 | 你可以看到,key-01、key-03 都不受影响,只有 key-02 需要被迁移节点 D。 124 | 125 | 假设节点数量从 3 减少到了 2,比如将节点 A 移除: 126 | 127 | ![](https://img-blog.csdnimg.cn/img_convert/31485046f1303b57d8aaeaab103ea7ab.png) 128 | 129 | 你可以看到,key-02 和 key-03 不会受到影响,只有 key-01 需要被迁移节点 B。 130 | 131 | 因此,**在一致哈希算法中,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响**。 132 | 133 | 上面这些图中 3 个节点映射在哈希环还是比较分散的,所以看起来请求都会「均衡」到每个节点。 134 | 135 | 但是**一致性哈希算法并不保证节点能够在哈希环上分布均匀**,这样就会带来一个问题,会有大量的请求集中在一个节点上。 136 | 137 | 比如,下图中 3 个节点的映射位置都在哈希环的右半边: 138 | 139 | ![](https://img-blog.csdnimg.cn/img_convert/d528bae6fcec2357ba2eb8f324ad9fd5.png) 140 | 141 | 这时候有一半以上的数据的寻址都会找节点 A,也就是访问请求主要集中的节点 A 上,这肯定不行的呀,说好的负载均衡呢,这种情况一点都不均衡。 142 | 143 | 另外,在这种节点分布不均匀的情况下,进行容灾与扩容时,哈希环上的相邻节点容易受到过大影响,容易发生雪崩式的连锁反应。 144 | 145 | 比如,上图中如果节点 A 被移除了,当节点 A 宕机后,根据一致性哈希算法的规则,其上数据应该全部迁移到相邻的节点 B 上,这样,节点 B 的数据量、访问量都会迅速增加很多倍,一旦新增的压力超过了节点 B 的处理能力上限,就会导致节点 B 崩溃,进而形成雪崩式的连锁反应。 146 | 147 | 所以,**一致性哈希算法虽然减少了数据迁移量,但是存在节点分布不均匀的问题**。 148 | 149 | ## 如何通过虚拟节点提高均衡度? 150 | 151 | 要想解决节点能在哈希环上分配不均匀的问题,就是要有大量的节点,节点数越多,哈希环上的节点分布的就越均匀。 152 | 153 | 但问题是,实际中我们没有那么多节点。所以这个时候我们就加入**虚拟节点**,也就是对一个真实节点做多个副本。 154 | 155 | 具体做法是,**不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系。** 156 | 157 | 比如对每个节点分别设置 3 个虚拟节点: 158 | 159 | - 对节点 A 加上编号来作为虚拟节点:A-01、A-02、A-03 160 | - 对节点 B 加上编号来作为虚拟节点:B-01、B-02、B-03 161 | - 对节点 C 加上编号来作为虚拟节点:C-01、C-02、C-03 162 | 163 | 引入虚拟节点后,原本哈希环上只有 3 个节点的情况,就会变成有 9 个虚拟节点映射到哈希环上,哈希环上的节点数量多了 3 倍。 164 | 165 | ![](https://img-blog.csdnimg.cn/img_convert/dbb57b8d6071d011d05eeadd93269e13.png) 166 | 167 | 你可以看到,**节点数量多了后,节点在哈希环上的分布就相对均匀了**。这时候,如果有访问请求寻址到「A-01」这个虚拟节点,接着再通过「A-01」虚拟节点找到真实节点 A,这样请求就能访问到真实节点 A 了。 168 | 169 | 上面为了方便你理解,每个真实节点仅包含 3 个虚拟节点,这样能起到的均衡效果其实很有限。而在实际的工程中,虚拟节点的数量会大很多,比如 Nginx 的一致性哈希算法,每个权重为 1 的真实节点就含有 160 个虚拟节点。 170 | 171 | 另外,虚拟节点除了会提高节点的均衡度,还会提高系统的稳定性。**当节点变化时,会有不同的节点共同分担系统的变化,因此稳定性更高**。 172 | 173 | 比如,当某个节点被移除时,对应该节点的多个虚拟节点均会移除,而这些虚拟节点按顺时针方向的下一个虚拟节点,可能会对应不同的真实节点,即这些不同的真实节点共同分担了节点变化导致的压力。 174 | 175 | 而且,有了虚拟节点后,还可以为硬件配置更好的节点增加权重,比如对权重更高的节点增加更多的虚拟机节点即可。 176 | 177 | 因此,**带虚拟节点的一致性哈希方法不仅适合硬件配置不同的节点的场景,而且适合节点规模会发生变化的场景**。 178 | 179 | ## 总结 180 | 181 | 不同的负载均衡算法适用的业务场景也不同的。 182 | 183 | 轮询这类的策略只能适用与每个节点的数据都是相同的场景,访问任意节点都能请求到数据。但是不适用分布式系统,因为分布式系统意味着数据水平切分到了不同的节点上,访问数据的时候,一定要寻址存储该数据的节点。 184 | 185 | 哈希算法虽然能建立数据和节点的映射关系,但是每次在节点数量发生变化的时候,最坏情况下所有数据都需要迁移,这样太麻烦了,所以不适用节点数量变化的场景。 186 | 187 | 为了减少迁移的数据量,就出现了一致性哈希算法。 188 | 189 | 一致性哈希是指将「存储节点」和「数据」都映射到一个首尾相连的哈希环上,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响。 190 | 191 | 但是一致性哈希算法不能够均匀的分布节点,会出现大量请求都集中在一个节点的情况,在这种情况下进行容灾与扩容时,容易出现雪崩的连锁反应。 192 | 193 | 为了解决一致性哈希算法不能够均匀的分布节点的问题,就需要引入虚拟节点,对一个真实节点做多个副本。不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系。 194 | 195 | 引入虚拟节点后,会提高节点的均衡度,还会提高系统的稳定性。所以,带虚拟节点的一致性哈希方法不仅适合硬件配置不同的节点的场景,而且适合节点规模会发生变化的场景。 196 | 197 | 完! 198 | 199 | ## 关注作者 200 | 201 | **_哈喽,我是小林,就爱图解计算机基础,如果觉得文章对你有帮助,欢迎微信搜索「小林 coding」,关注后,回复「网络」再送你图解网络 PDF_** 202 | 203 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/其他/公众号介绍.png) 204 | -------------------------------------------------------------------------------- /os/9_linux_cmd/linux_network.md: -------------------------------------------------------------------------------- 1 | # 10.1 如何查看网络的性能指标? 2 | 3 | Linux 网络协议栈是根据 TCP/IP 模型来实现的,TCP/IP 模型由应用层、传输层、网络层和网络接口层,共四层组成,每一层都有各自的职责。 4 | 5 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/操作系统/浮点/封装.png) 6 | 7 | 应用程序要发送数据包时,通常是通过 socket 接口,于是就会发生系统调用,把应用层的数据拷贝到内核里的 socket 层,接着由网络协议栈从上到下逐层处理后,最后才会送到网卡发送出去。 8 | 9 | 而对于接收网络包时,同样也要经过网络协议逐层处理,不过处理的方向与发送数据时是相反的,也就是从下到上的逐层处理,最后才送到应用程序。 10 | 11 | 网络的速度往往跟用户体验是挂钩的,那我们又该用什么指标来衡量 Linux 的网络性能呢?以及如何分析网络问题呢? 12 | 13 | 这次,我们就来说这些。 14 | 15 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/操作系统/网络/网络提纲.png) 16 | 17 | --- 18 | 19 | ## 性能指标有哪些? 20 | 21 | 通常是以 4 个指标来衡量网络的性能,分别是带宽、延时、吞吐率、PPS(Packet Per Second),它们表示的意义如下: 22 | 23 | - *带宽*,表示链路的最大传输速率,单位是 b/s(比特 / 秒),带宽越大,其传输能力就越强。 24 | - *延时*,表示请求数据包发送后,收到对端响应,所需要的时间延迟。不同的场景有着不同的含义,比如可以表示建立 TCP 连接所需的时间延迟,或一个数据包往返所需的时间延迟。 25 | - *吞吐率*,表示单位时间内成功传输的数据量,单位是 b/s(比特 / 秒)或者 B/s(字节 / 秒),吞吐受带宽限制,带宽越大,吞吐率的上限才可能越高。 26 | - *PPS*,全称是 Packet Per Second(包 / 秒),表示以网络包为单位的传输速率,一般用来评估系统对于网络的转发能力。 27 | 28 | 29 | 当然,除了以上这四种基本的指标,还有一些其他常用的性能指标,比如: 30 | 31 | - *网络的可用性*,表示网络能否正常通信; 32 | - *并发连接数*,表示 TCP 连接数量; 33 | - *丢包率*,表示所丢失数据包数量占所发送数据组的比率; 34 | - *重传率*,表示重传网络包的比例; 35 | 36 | 你可能会问了,如何观测这些性能指标呢?不急,继续往下看。 37 | 38 | --- 39 | 40 | ## 网络配置如何看? 41 | 42 | 要想知道网络的配置和状态,我们可以使用 `ifconfig` 或者 `ip` 命令来查看。 43 | 44 | 这两个命令功能都差不多,不过它们属于不同的软件包,`ifconfig` 属于 `net-tools` 软件包,`ip` 属于 `iproute2` 软件包,我的印象中 `net-tools` 软件包没有人继续维护了,而 `iproute2` 软件包是有开发者依然在维护,所以更推荐你使用 `ip` 工具。 45 | 46 | 47 | 学以致用,那就来使用这两个命令,来查看网口 `eth0` 的配置等信息: 48 | 49 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/操作系统/网络/showeth0.png) 50 | 51 | 52 | 虽然这两个命令输出的格式不尽相同,但是输出的内容基本相同,比如都包含了 IP 地址、子网掩码、MAC 地址、网关地址、MTU 大小、网口的状态以及网路包收发的统计信息,下面就来说说这些信息,它们都与网络性能有一定的关系。 53 | 54 | 第一,网口的连接状态标志。其实也就是表示对应的网口是否连接到交换机或路由器等设备,如果 `ifconfig` 输出中看到有 `RUNNING`,或者 `ip` 输出中有 `LOWER_UP`,则说明物理网路是连通的,如果看不到,则表示网口没有接网线。 55 | 56 | 第二,MTU 大小。默认值是 `1500` 字节,其作用主要是限制网络包的大小,如果 IP 层有一个数据报要传,而且数据帧的长度比链路层的 MTU 还大,那么 IP 层就需要进行分片,即把数据报分成若干片,这样每一片就都小于 MTU。事实上,每个网络的链路层 MTU 可能会不一样,所以你可能需要调大或者调小 MTU 的数值。 57 | 58 | 第三,网口的 IP 地址、子网掩码、MAC 地址、网关地址。这些信息必须要配置正确,网络功能才能正常工作。 59 | 60 | 第四,网路包收发的统计信息。通常有网络收发的字节数、包数、错误数以及丢包情况的信息,如果 `TX`(发送)和 `RX`(接收)部分中 errors、dropped、overruns、carrier 以及 collisions 等指标不为 0 时,则说明网络发送或者接收出问题了,这些出错统计信息的指标意义如下: 61 | 62 | - *errors* 表示发生错误的数据包数,比如校验错误、帧同步错误等; 63 | - *dropped* 表示丢弃的数据包数,即数据包已经收到了 Ring Buffer(这个缓冲区是在内核内存中,更具体一点是在网卡驱动程序里),但因为系统内存不足等原因而发生的丢包; 64 | - *overruns* 表示超限数据包数,即网络接收/发送速度过快,导致 Ring Buffer 中的数据包来不及处理,而导致的丢包,因为过多的数据包挤压在 Ring Buffer,这样 Ring Buffer 很容易就溢出了; 65 | - *carrier* 表示发生 carrirer 错误的数据包数,比如双工模式不匹配、物理电缆出现问题等; 66 | - *collisions* 表示冲突、碰撞数据包数; 67 | 68 | `ifconfig` 和 `ip` 命令只显示的是网口的配置以及收发数据包的统计信息,而看不到协议栈里的信息,那接下来就来看看如何查看协议栈里的信息。 69 | 70 | --- 71 | 72 | ## socket 信息如何查看? 73 | 74 | 我们可以使用 `netstat` 或者 `ss`,这两个命令查看 socket、网络协议栈、网口以及路由表的信息。 75 | 76 | 虽然 `netstat` 与 `ss` 命令查看的信息都差不多,但是如果在生产环境中要查看这类信息的时候,尽量不要使用 `netstat` 命令,因为它的性能不好,在系统比较繁忙的情况下,如果频繁使用 `netstat` 命令则会对性能的开销雪上加霜,所以更推荐你使用性能更好的 `ss` 命令。 77 | 78 | 从下面这张图,你可以看到这两个命令的输出内容: 79 | 80 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/操作系统/网络/showsocket.png) 81 | 82 | 83 | 84 | 可以发现,输出的内容都差不多,比如都包含了 socket 的状态(*State*)、接收队列(*Recv-Q*)、发送队列(*Send-Q*)、本地地址(*Local Address*)、远端地址(*Foreign Address*)、进程 PID 和进程名称(*PID/Program name*)等。 85 | 86 | 接收队列(*Recv-Q*)和发送队列(*Send-Q*)比较特殊,在不同的 socket 状态。它们表示的含义是不同的。 87 | 88 | 当 socket 状态处于 `Established`时: 89 | 90 | - *Recv-Q* 表示 socket 缓冲区中还没有被应用程序读取的字节数; 91 | - *Send-Q* 表示 socket 缓冲区中还没有被远端主机确认的字节数; 92 | 93 | 而当 socket 状态处于 `Listen` 时: 94 | 95 | - *Recv-Q* 表示全连接队列的长度; 96 | - *Send-Q* 表示全连接队列的最大长度; 97 | 98 | 99 | 在 TCP 三次握手过程中,当服务器收到客户端的 SYN 包后,内核会把该连接存储到半连接队列,然后再向客户端发送 SYN+ACK 包,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其增加到全连接队列,等待进程调用 `accept()` 函数时把连接取出来。 100 | 101 | ![半连接队列与全连接队列](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8D%8A%E8%BF%9E%E6%8E%A5%E5%92%8C%E5%85%A8%E8%BF%9E%E6%8E%A5/3.jpg) 102 | 103 | 104 | 也就说,全连接队列指的是服务器与客户端完了 TCP 三次握手后,还没有被 `accept()` 系统调用取走连接的队列。 105 | 106 | 107 | 那对于协议栈的统计信息,依然还是使用 `netstat` 或 `ss`,它们查看统计信息的命令如下: 108 | 109 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/操作系统/网络/showinfo.png) 110 | 111 | 112 | `ss` 命令输出的统计信息相比 `netsat` 比较少,`ss` 只显示已经连接(*estab*)、关闭(*closed*)、孤儿(*orphaned*)socket 等简要统计。 113 | 114 | 而 `netstat` 则有更详细的网络协议栈信息,比如上面显示了 TCP 协议的主动连接(*active connections openings*)、被动连接(*passive connection openings*)、失败重试(*failed connection attempts*)、发送(*segments send out*)和接收(*segments received*)的分段数量等各种信息。 115 | 116 | --- 117 | 118 | ## 网络吞吐率和 PPS 如何查看? 119 | 120 | 可以使用 `sar` 命令当前网络的吞吐率和 PPS,用法是给 `sar` 增加 `-n` 参数就可以查看网络的统计信息,比如 121 | 122 | - sar -n DEV,显示网口的统计数据; 123 | - sar -n EDEV,显示关于网络错误的统计数据; 124 | - sar -n TCP,显示 TCP 的统计数据 125 | 126 | 比如,我通过 `sar` 命令获取了网口的统计信息: 127 | 128 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/操作系统/网络/sar.png) 129 | 130 | 131 | 它们的含义: 132 | 133 | - `rxpck/s` 和 `txpck/s` 分别是接收和发送的 PPS,单位为包 / 秒。 134 | - `rxkB/s` 和 `txkB/s` 分别是接收和发送的吞吐率,单位是 KB/ 秒。 135 | - `rxcmp/s` 和 `txcmp/s` 分别是接收和发送的压缩数据包数,单位是包 / 秒。 136 | 137 | 对于带宽,我们可以使用 `ethtool` 命令来查询,它的单位通常是 `Gb/s` 或者 `Mb/s`,不过注意这里小写字母 `b` ,表示比特而不是字节。我们通常提到的千兆网卡、万兆网卡等,单位也都是比特(*bit*)。如下你可以看到,eth0 网卡就是一个千兆网卡: 138 | 139 | ```bash 140 | $ ethtool eth0 | grep Speed 141 | Speed: 1000Mb/s 142 | ``` 143 | 144 | --- 145 | 146 | ## 连通性和延时如何查看? 147 | 148 | 要测试本机与远程主机的连通性和延时,通常是使用 `ping` 命令,它是基于 ICMP 协议的,工作在网络层。 149 | 150 | 比如,如果要测试本机到 `192.168.12.20` IP 地址的连通性和延时: 151 | 152 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/操作系统/网络/ping.png) 153 | 154 | 显示的内容主要包含 `icmp_seq`(ICMP 序列号)、`TTL`(生存时间,或者跳数)以及 `time` (往返延时),而且最后会汇总本次测试的情况,如果网络没有丢包,`packet loss` 的百分比就是 0。 155 | 156 | 不过,需要注意的是,`ping` 不通服务器并不代表 HTTP 请求也不通,因为有的服务器的防火墙是会禁用 ICMP 协议的。 157 | 158 | 159 | --- 160 | 161 | ## 关注作者 162 | 163 | ***哈喽,我是小林,就爱图解计算机基础,如果觉得文章对你有帮助,欢迎微信搜索「小林 coding」,关注后,回复「网络」再送你图解网络 PDF*** 164 | 165 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/其他/公众号介绍.png) 166 | 167 | 168 | -------------------------------------------------------------------------------- /os/9_linux_cmd/pv_uv.md: -------------------------------------------------------------------------------- 1 | # 10.2 如何从日志分析 PV、UV? 2 | 3 | 4 | 5 | 很多时候,我们观察程序是否如期运行,或者是否有错误,最直接的方式就是看运行**日志**,当然要想从日志快速查到我们想要的信息,前提是程序打印的日志要精炼、精准。 6 | 7 | 但日志涵盖的信息远不止于此,比如对于 nginx 的 access.log 日志,我们可以根据日志信息**分析用户行为**。 8 | 9 | 什么用户行为呢?比如分析出哪个页面访问次数(*PV*)最多,访问人数(*UV*)最多,以及哪天访问量最多,哪个请求访问最多等等。 10 | 11 | 这次,将用一个大概几万条记录的 nginx 日志文件作为案例,一起来看看如何分析出「用户信息」。 12 | 13 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/提纲日志.png) 14 | 15 | 16 | 17 | --- 18 | 19 | 20 | ## 别急着开始 21 | 22 | 当我们要分析日志的时候,先用 `ls -lh` 命令查看日志文件的大小,如果日志文件大小非常大,最好不要在线上环境做。 23 | 24 | 比如我下面这个日志就 6.5M,不算大,在线上环境分析问题不大。 25 | 26 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/ls.png) 27 | 28 | 如果日志文件数据量太大,你直接一个 `cat` 命令一执行,是会影响线上环境,加重服务器的负载,严重的话,可能导致服务器无响应。 29 | 30 | 当发现日志很大的时候,我们可以使用 `scp` 命令将文件传输到闲置的服务器再分析,scp 命令使用方式如下图: 31 | 32 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/scp.png) 33 | 34 | 35 | --- 36 | 37 | ## 慎用 cat 38 | 39 | 大家都知道 `cat` 命令是用来查看文件内容的,但是日志文件数据量有多少,它就读多少,很显然不适用大文件。 40 | 41 | 对于大文件,我们应该养成好习惯,用 `less` 命令去读文件里的内容,因为 less 并不会加载整个文件,而是按需加载,先是输出一小页的内容,当你要往下看的时候,才会继续加载。 42 | 43 | 44 | 45 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/less.png) 46 | 47 | 48 | 49 | 可以发现,nginx 的 access.log 日志每一行是一次用户访问的记录,从左到右分别包含如下信息: 50 | 51 | - 客户端的 IP 地址; 52 | - 访问时间; 53 | - HTTP 请求的方法、路径、协议版本、协议版本、返回的状态码; 54 | - User Agent,一般是客户端使用的操作系统以及版本、浏览器及版本等; 55 | 56 | 57 | 不过,有时候我们想看日志最新部分的内容,可以使用 `tail` 命令,比如当你想查看倒数 5 行的内容,你可以使用这样的命令: 58 | 59 | 60 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/tail-n.png) 61 | 62 | 63 | 64 | 如果你想实时看日志打印的内容,你可以使用 `tail -f` 命令,这样你看日志的时候,就会是阻塞状态,有新日志输出的时候,就会实时显示出来。 65 | 66 | 67 | --- 68 | 69 | ## PV 分析 70 | 71 | 72 | PV 的全称叫 *Page View*,用户访问一个页面就是一次 PV,比如大多数博客平台,点击一次页面,阅读量就加 1,所以说 PV 的数量并不代表真实的用户数量,只是个点击量。 73 | 74 | 对于 nginx 的 `acess.log` 日志文件来说,分析 PV 还是比较容易的,既然日志里的内容是访问记录,那有多少条日志记录就有多少 PV。 75 | 76 | 我们直接使用 `wc -l` 命令,就可以查看整体的 PV 了,如下图一共有 49903 条 PV。 77 | 78 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/wc.png) 79 | 80 | 81 | 82 | 83 | --- 84 | 85 | ## PV 分组 86 | 87 | nginx 的 `acess.log` 日志文件有访问时间的信息,因此我们可以根据访问时间进行分组,比如按天分组,查看每天的总 PV,这样可以得到更加直观的数据。 88 | 89 | 要按时间分组,首先我们先「访问时间」过滤出来,这里可以使用 awk 命令来处理,awk 是一个处理文本的利器。 90 | 91 | awk 命令默认是以「空格」为分隔符,由于访问时间在日志里的第 4 列,因此可以使用 `awk '{print $4}' access.log` 命令把访问时间的信息过滤出来,结果如下: 92 | 93 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/awk日期.png) 94 | 95 | 96 | 97 | 98 | 99 | 上面的信息还包含了时分秒,如果只想显示年月日的信息,可以使用 `awk` 的 `substr` 函数,从第 2 个字符开始,截取 11 个字符。 100 | 101 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/awk日期2.png) 102 | 103 | 104 | 105 | 106 | 接着,我们可以使用 `sort` 对日期进行排序,然后使用 `uniq -c` 进行统计,于是按天分组的 PV 就出来了。 107 | 108 | 可以看到,每天的 PV 量大概在 2000-2800: 109 | 110 | 111 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/awkpv.png) 112 | 113 | 114 | 注意,**使用 `uniq -c` 命令前,先要进行 `sort` 排序**,因为 uniq 去重的原理是比较相邻的行,然后除去第二行和该行的后续副本,因此在使用 uniq 命令之前,请使用 sort 命令使所有重复行相邻。 115 | 116 | --- 117 | 118 | ## UV 分析 119 | 120 | UV 的全称是 *Uniq Visitor*,它代表访问人数,比如公众号的阅读量就是以 UV 统计的,不管单个用户点击了多少次,最终只算 1 次阅读量。 121 | 122 | access.log 日志里虽然没有用户的身份信息,但是我们可以用「客户端 IP 地址」来**近似统计** UV。 123 | 124 | 125 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/uv.png) 126 | 127 | 该命令的输出结果是 2589,也就说明 UV 的量为 2589。上图中,从左到右的命令意思如下: 128 | 129 | - `awk '{print $1}' access.log`,取日志的第 1 列内容,客户端的 IP 地址正是第 1 列; 130 | - `sort`,对信息排序; 131 | - `uniq`,去除重复的记录; 132 | - `wc -l`,查看记录条数; 133 | 134 | --- 135 | 136 | ## UV 分组 137 | 138 | 假设我们按天来分组分析每天的 UV 数量,这种情况就稍微比较复杂,需要比较多的命令来实现。 139 | 140 | 既然要按天统计 UV,那就得把「日期 + IP 地址」过滤出来,并去重,命令如下: 141 | 142 | 143 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/uv分组.png) 144 | 145 | 146 | 具体分析如下: 147 | 148 | - 第一次 `ack` 是将第 4 列的日期和第 1 列的客户端 IP 地址过滤出来,并用空格拼接起来; 149 | - 然后 `sort` 对第一次 ack 输出的内容进行排序; 150 | - 接着用 `uniq` 去除重复的记录,也就说日期 +IP 相同的行就只保留一个; 151 | 152 | 上面只是把 UV 的数据列了出来,但是并没有统计出次数。 153 | 154 | 如果需要对当天的 UV 统计,在上面的命令再拼接 `awk '{uv[$1]++;next}END{for (ip in uv) print ip, uv[ip]}'` 命令就可以了,结果如下图: 155 | 156 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/awknext.png) 157 | 158 | awk 本身是「逐行」进行处理的,当执行完一行后,我们可以用 `next` 关键字来告诉 awk 跳转到下一行,把下一行作为输入。 159 | 160 | 对每一行输入,awk 会根据第 1 列的字符串(也就是日期)进行累加,这样相同日期的 ip 地址,就会累加起来,作为当天的 uv 数量。 161 | 162 | 之后的 `END` 关键字代表一个触发器,就是当前面的输入全部完成后,才会执行 END {} 中的语句,END 的语句是通过 foreach 遍历 uv 中所有的 key,打印出按天分组的 uv 数量。 163 | 164 | --- 165 | 166 | ## 终端分析 167 | 168 | nginx 的 access.log 日志最末尾关于 User Agent 的信息,主要是客户端访问服务器使用的工具,可能是手机、浏览器等。 169 | 170 | 因此,我们可以利用这一信息来分析有哪些终端访问了服务器。 171 | 172 | User Agent 的信息在日志里的第 12 列,因此我们先使用 `awk` 过滤出第 12 列的内容后,进行 `sort` 排序,再用 `uniq -c` 去重并统计,最后再使用 `sort -rn`(*r 表示逆向排序,n 表示按数值排序*)对统计的结果排序,结果如下图: 173 | 174 | 175 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/terminal.png) 176 | 177 | --- 178 | 179 | ## 分析 TOP3 的请求 180 | 181 | access.log 日志中,第 7 列是客户端请求的路径,先使用 `awk` 过滤出第 7 列的内容后,进行 `sort` 排序,再用 `uniq -c` 去重并统计,然后再使用 `sort -rn` 对统计的结果排序,最后使用 `head -n 3` 分析 TOP3 的请求,结果如下图: 182 | 183 | 184 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/网络/log/TOP3.png) 185 | 186 | --- 187 | 188 | ## 关注作者 189 | 190 | ***哈喽,我是小林,就爱图解计算机基础,如果觉得文章对你有帮助,欢迎微信搜索「小林 coding」,关注后,回复「网络」再送你图解网络 PDF*** 191 | 192 | ![](https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost3@main/其他/公众号介绍.png) -------------------------------------------------------------------------------- /os/README.md: -------------------------------------------------------------------------------- 1 | # 图解系统介绍 2 | 3 | 大家好,我是小林,是《图解系统》的作者,本站的内容都是整理于我[公众号](https://mp.weixin.qq.com/s/FYH1I8CRsuXDSybSGY_AFA)里的图解文章。 4 | 5 | 还没关注的朋友,可以微信搜索「**小林 coding**」,关注我的公众号,**后续最新版本的 PDF 会在我的公众号第一时间发布**,而且会有更多其他系列的图解文章,比如操作系统、计算机组成、数据库、算法等等。 6 | 7 | 简单介绍下这个《图解系统》,整个内容共有 **`16W` 字 + `400` 张图**,文字都是小林一个字一个字敲出来的,图片都是小林一个点一条线画出来的,非常的不容易。 8 | 9 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/38c89e02026a4c1e8b98ed0a9ee6cb44.png) 10 | 11 | 图解系统不仅仅涉及了操作系统的内容,还涉及一些计算机组成和 Linux 命令的内容, 12 | 13 | 当然还是操作系统的内容占比较高,基本把操作系统**进程管理、内存管理、文件系统、设备管理、网络系统**这五大结构图解了,其中进程管理和网络系统这两个章节篇幅比较多,进程管理不仅包含了进程与线程的基本知识,还包含了进程间通信,多线程同步、死锁、悲观锁和乐观锁。网络系统包含 I/O 多路复用、零拷贝、Reactor 等等。 14 | 15 | 计算机组成主要涉及是 CPU 方面的知识,我们不关注 CPU 是怎么设计与实现的,**只关注跟我们开发者有关系的 CPU 知识**,比如 CPU 执行程序的原理,CPU 缓存,CPU 伪共享等等,这些看似跟我们开发者无关,实际上关系挺大的,只有了解 CPU 缓存才能写出更快的代码,只要了解 CPU 伪共享才能避免写出无效缓存的代码。 16 | 17 | 至于 Linux 命令的章节暂时内容没有很多,主要就写了如何用 Linux 命令「查看网络指标」和「从日志分析 PV、UV」,之所以没有写太多是觉得命令类的文章没办法体现出小林的图解功力,再加上这类命令一般网上资源也很多,工作中遇到需要使用某个命令时,去搜索了解并自己体验了一番后,才会比较深刻,单纯只看文章很容易就忘记这些命令了。 18 | 19 | ## 小白适合看吗? 20 | 21 | 《图解系统》不是单纯的面经,而是相对比较系统化的内容,当然小林所写的内容是操作系统的重点知识,也是面试常问的知识点。 22 | 23 | 我觉得相比背零零散散的面经,更建议你学好整个操作系统的知识体系,后面你在看面经的时候,你会发觉这些只不过是这颗知识树中的一个小分支,而且延展性会更好。 24 | 25 | 操作系统是很容易让小白畏惧一门课,因为不管哪本操作系统书都是厚厚的,就会觉得操作系统东西太多,而且也不容易看懂,每个字我们能得懂,但是连成一句话就看懵了。 26 | 27 | 其实小林当时在入门操作系统的时候,也是跟大家感受一样的,谁不是从小白度过过来的呢? 28 | 29 | 之前我花了很多时间看书和看视频,学好操作系统后,我就在想能不能写一份帮助大家快速入门操作系统系统文章呢,于是就开始踏上了图解之路,**用精美的图片打破大家对操作系统的畏惧感**。 30 | 31 | 事实证明,图解系列是正确的,在公众号连续写了很多篇图解系统的文章后,收到了非常多读者的支持与认可,有反馈以前大学没学会的,然后看了我的文章突然就醒悟了,也有反馈面试前突击了我的文章,然后拿到了心意的 offer。 32 | 33 | 所以,这份图解系统适合小白学习,也可以当作面试突击用的手册。 34 | 35 | 不过,再怎么吹我的《图解系统》,如果大家想要系统化全面的学习操作系统,自然还是离不开书的,《图解系统》的末尾会有我学习操作系统的心得,会推荐我看过并且认为不错的书和视频,大家可以留意一下。 36 | 37 | ## 要怎么阅读? 38 | 39 | 很诚恳的告诉你,《图解系统》不是教科书。而是我在公众号里写的图解系统文章的整合,所以肯定是没有教科书那么细致和全面,当然也就不会有很多废话,都是直击重点,不绕弯,而且有的知识点书上看不到。 40 | 41 | 阅读的顺序可以不用从头读到尾,你可以根据你想要了解的知识点,通过本站的搜索功能,去看哪个章节的文章就好,可以随意阅读任何章节。 42 | 43 | 本站的左侧菜单就是《图解系统》的目录结构(别看篇章不多,每一章都是很长很长的文章哦 :laughing:): 44 | 45 | - **硬件结构** :point_down: 46 | - [CPU 是如何执行程序的?](/os/1_hardware/how_cpu_run.md) 47 | - [磁盘比内存慢几万倍?](/os/1_hardware/storage.md) 48 | - [如何写出让 CPU 跑得更快的代码?](/os/1_hardware/how_to_make_cpu_run_faster.md) 49 | - [CPU 缓存一致性](/os/1_hardware/cpu_mesi.md) 50 | - [CPU 是如何执行任务的?](/os/1_hardware/how_cpu_deal_task.md) 51 | - [什么是软中断?](/os/1_hardware/soft_interrupt.md) 52 | - [为什么 0.1 + 0.2 不等于 0.3?](/os/1_hardware/float.md) 53 | - **操作系统结构** :point_down: 54 | - [Linux 内核 vs Windows 内核](/os/2_os_structure/linux_vs_windows.md) 55 | - **内存管理** :point_down: 56 | - [为什么要有虚拟内存?](/os/3_memory/vmem.md) 57 | - [malloc 是如何分配内存的?](/os/3_memory/malloc.md) 58 | - [内存满了,会发生什么?](/os/3_memory/mem_reclaim.md) 59 | - [在 4GB 物理内存的机器上,申请 8G 内存会怎么样?](/os/3_memory/alloc_mem.md) 60 | - [如何避免预读失效和缓存污染的问题?](/os/3_memory/cache_lru.md) 61 | - [深入理解 Linux 虚拟内存管理](/os/3_memory/linux_mem.md) 62 | - **进程管理** :point_down: 63 | - [进程、线程基础知识](/os/4_process/process_base.md) 64 | - [进程间有哪些通信方式?](/os/4_process/process_commu.md) 65 | - [多线程冲突了怎么办?](/os/4_process/multithread_sync.md) 66 | - [怎么避免死锁?](/os/4_process/deadlock.md) 67 | - [什么是悲观锁、乐观锁?](/os/4_process/pessim_and_optimi_lock.md) 68 | - [一个进程最多可以创建多少个线程?](/os/4_process/create_thread_max.md) 69 | - [线程崩溃了,进程也会崩溃吗?](/os/4_process/thread_crash.md) 70 | - **调度算法** :point_down: 71 | - [进程调度/页面置换/磁盘调度算法](/os/5_schedule/schedule.md) 72 | - **文件系统** :point_down: 73 | - [文件系统全家桶](/os/6_file_system/file_system.md) 74 | - [进程写文件时,进程发生了崩溃,已写入的数据会丢失吗?](/os/6_file_system/pagecache.md) 75 | - **设备管理** :point_down: 76 | - [键盘敲入 A 字母时,操作系统期间发生了什么?](/os/7_device/device.md) 77 | - **网络系统** :point_down: 78 | - [什么是零拷贝?](/os/8_network_system/zero_copy.md) 79 | - [I/O 多路复用:select/poll/epoll](/os/8_network_system/selete_poll_epoll.md) 80 | - [高性能网络模式:Reactor 和 Proactor](/os/8_network_system/reactor.md) 81 | - [什么是一致性哈希?](/os/8_network_system/hash.md) 82 | - **Linux 命令** :point_down: 83 | - [如何查看网络的性能指标?](/os/9_linux_cmd/linux_network.md) 84 | - [如何从日志分析 PV、UV?](/os/9_linux_cmd/pv_uv.md) 85 | - **学习心得** :point_down: 86 | - [操作系统怎么学?](/os/10_learn/learn_os.md) 87 | - [画图经验分享](/os/10_learn/draw.md) 88 | 89 | ## 有错误怎么办? 90 | 91 | 小林是个手残党,时常写出错别字。 92 | 93 | 如果你在学习的过程中,**如果你发现有任何错误或者疑惑的地方,欢迎你通过邮箱或者底部留言给小林**,勘误邮箱:xiaolincoding@163.com 94 | 95 | 小林抽时间会逐个修正,然后发布新版本的图解系统 PDF,一起迭代出更好的图解系统! 96 | 97 | 新的图解文章都在公众号首发,别忘记关注了哦!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 98 | 99 | ![](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/其他/公众号介绍.png) -------------------------------------------------------------------------------- /reader_nb/3_reader.md: -------------------------------------------------------------------------------- 1 | # 三本逆袭!拿到京东美团等 offer 2 | 3 | 大家好,我是小林哥。 4 | 5 | 这次还是读者牛逼系列,最近发这个频繁主要是准备秋招了嘛。 6 | 7 | 想通过几篇读者的校招心得激励一下还在校的读者,给大家打打鸡血,让大家感觉自己又可以了。 8 | 9 | 我觉得这个挺重要的,因为当你发现别人都可以的时候,其实也是变相在督促自己,告诉自己你也是可以的,别低估努力后的自己。 10 | 11 | 这次读者他是一名三本的大四学生,通过去年 6 个月的努力,校招时拿到京东美团滴滴高途作业帮等 offer,最终选择了在作业帮担任后端研发。 12 | 13 | 这又是一个逆袭的例子,所以还是那句话,**以大多数人的努力程度之低,根本轮不到拼天赋**。 14 | 15 | 今天就由这位读者向大家分享一下,他的曲折的求职之路和这一路走下来收获到的经验和感悟。 16 | 17 | ------ 18 | 19 | ## 转折点 20 | 21 | #### 2020-2-29 我的想法开始发生变化 22 | 23 | 疫情前,我是一个不爱学习的井底之蛙,喜欢带着鄙夷的目光看这个世界。 24 | 25 | 直到疫情来袭,困在家中无所事事,游戏也打腻了。 26 | 27 | 当时记得很清楚,某 up 主介绍 Nignx 的时候,我第一反应,这是啥,这个怎么念? 28 | 29 | 随着这三分钟热度,想再学习一下 Java 基础吧。 30 | 31 | Java 基础那时我已经看了很多遍视频了,觉得不能再这样下去了,以后找工作不能再让爸妈为我操心,突然觉得当年说的话简直就…… 32 | 33 | ![图片](https://img-blog.csdnimg.cn/img_convert/94d9f56e89e1ac542e70a076c9e3be78.png) 34 | 35 | 于是,决定要走出这个舒适圈。我就把网上好评的 Java 相关的书籍全买下来了。 36 | 37 | ![图片](https://img-blog.csdnimg.cn/img_convert/47897936edcc649dfaddbb7e30a432f9.png) 38 | 39 | #### 2020-3-10 开干! 40 | 41 | 书一到,立马按排! 42 | 43 | **每天 7 点起床,吃完饭就开干,一直到晚上 23 点。** 44 | 45 | 就这样,因为这个疫情,我从一个不爱看书转变成了抱着书就不肯撒手的人。 46 | 47 | 其实,书上的知识真的可以充饥。 48 | 49 | ## 第一份简历 50 | 51 | #### 2020-4-2 投递了第一份简历 52 | 53 | 在某某人的催促下,我制作了我的第一份简历,没有照搬任何模板,没有花里胡哨,简单明了。 54 | 55 | ![图片](https://img-blog.csdnimg.cn/img_convert/46458343b65ffe0801d6fa85b565d74c.png) 56 | 57 | 我的简历当时写的很烂,毕竟简历就是你的第二张脸。 58 | 59 | 于是,我投了阿里,结果不了了之。 60 | 61 | ## 面试 62 | 63 | #### 2020-6-20 第一次接到面试邮件 64 | 65 | 当时,上海一家公司发出面试邀请。一面过二面挂,说实话,二面的感觉,完全就是敷衍我,能明显看到那种鄙视链的存在。 66 | 67 | 那时候深深的刻在了我的心中,自闭了有一阵。 68 | 69 | 但是我不相信宿命论,于是重新刷书做题,开始研究源码。 70 | 71 | #### 2020-8-23 从 0 到 1 72 | 73 | 之后投简历到处碰壁,于是转向一些小公司和外包公司。 74 | 75 | 这些公司面试,几乎不问技术问题,纯聊天。 76 | 77 | 就这样,都没有拿到 offer,不过经历上次面试,心态已经放平稳了。 78 | 79 | 后来终于拿到了一份 6.5k 的 offer,这是一个好的开始,我相信自己接下来会有更多 offer,于是继续准备继续面,我把目光开始转向中大厂。 80 | 81 | 最后在不断地坚持下,9 月中下旬 -10 月下,**收获了京东美团滴滴高途作业帮等 offer**。 82 | 83 | 最后询问了一些大佬,再比对薪资、部门、业务前景等等因素,最终选择了目前的公司。 84 | 85 | ## When 86 | 87 | 至于何时准备,我建议越早越好,毕竟一个萝卜一个坑。 88 | 89 | 前天坐飞机,认识了一个华科的小姐姐,非常优秀,人家从大一就开始参加这方面的社团和比赛,积累了不少经验和奖项,于是在 2020 年上半年就拿到了微软等国内外大厂实习机会。 90 | 91 | 所以,省下打游戏的时间吧,来做这些有意义的事情,为自己铺好后面的路,就不会像我一样很累了。 92 | 93 | ## How 94 | 95 | #### 怎么做? 96 | 97 | 三点:看书,刷题,参加比赛! 98 | 99 | 每个人不太一样,喜欢看视频学习,喜欢看电子书,或者背题。 100 | 101 | 而我就比较喜欢看纸质的书,书里一般讲的都是原理,当你明白原理,编程或者面试的时候就游刃有余了,只靠背题是不够的,背题只能提升你的下限,不能提升你的上限。 102 | 103 | 看完书也不要忘记动手实践,看看源码,或者自己实现一下,看看是不是这回事。顺便再刷一两道题,保持做题手感。当然能参加比赛就最好了,这是能把你和别人拉开距离的手段之一。 104 | 105 | **看书:**林鸽有一篇文章写的很好——《[看书的一点小建议](https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247493177&idx=1&sn=77e32cec53e8a1aee9fa9fd3b31b19c2&scene=21#wechat_redirect)》,我一开始看书也是看不懂,最后自己摸索出方法,和他说的差不多,当你能看懂并且看完一本书,幸福感爆棚。 106 | 107 | 并且,看书一定要思考,比如合适看完,看完有何感受,学到了什么,能否解决之前所学内容的困惑。 108 | 109 | 看完书,还是远远不够的,当你有一定能力了,可以翻阅一下官网的文档和源码,不要害怕看文档,其实通常这里都是权威且容易理解的。 110 | 111 | 看源码首先梳理流程,知道每步是干嘛的,都梳理明白之后,在去看细节如何实现的,最后再串起来。 112 | 113 | 推荐书籍:除了上面图中出现的书籍,顺便推荐一下 Go 方向的书籍,《Go 程序设计语言》、《Go 语言编程圣经》《GoWeb 编程》。 114 | 115 | **刷题:**刷题就用 leetcode 或者 lintcode,我是先刷数组、链表和树。明白这些数据结构后,我再去刷经典的 100 题。 116 | 117 | 每一题都要琢磨透,一天做 1 道都没有问题,只要你能弄明白就 ok。 118 | 119 | 其次当你遇到一道题二三十分钟都没有思路,直接看答案吧……这就是一个新的知识点,再遇到类似的题,起码你有思路了。 120 | 121 | 当你已经掌握做题套路后,也不要断了,保持做题的感觉。 122 | 123 | **参加比赛:**比赛就太多了,蓝桥杯 互联网 + ACM,越早参加这类的社团越好,还有机会培养你,当你有了一定的比赛经验,这也是丰富你简历的一个途径,有了这类奖项一定要在校招的时候好好利用起来。 124 | 125 | 平时关注一些技术博主,小林 coding、艾小仙等等。刷视频的时间,不如看看他们的文章,都是满满的干货! 126 | 127 | 再加个技术群和大家一起讨论讨论,或者给新手讲解,能给他讲明白,说明你已经掌握这个知识点了。 128 | 129 | #### 怎么投? 130 | 131 | 三点:牛客网,Boss,官网招聘公众号。 132 | 133 | 我这三个是我用的最多的,我先是打开手机的应用商城,搜每个模块的前几的 APP 公司,例如:在线教育,社交,游戏等。 134 | 135 | 然后从上面的进行投递(后来我才知道内推是个什么,最好先让别人内推,毕竟你过了,他有钱拿可以帮你指导一下),我基本都是官网投递的(都是辛酸史),不要嫌改简历麻烦,一定要改的对胃口! 136 | 137 | #### 怎么面? 138 | 139 | 面试前一定要准备,自我介绍一定要多练。 140 | 141 | 我和我室友经常面试前来一次模拟,以防磕巴。 142 | 143 | 面试的时候,场景题要有自己的想法,每个团队,面对校招生大多是看可培养性,不要无情的工具人。 144 | 145 | 而且要从容(面试官不开摄像头除外),语言逻辑一定要顺畅,突出重点,带有引导性的去和面试官聊。 146 | 147 | 举个例子,当时某厂面试官问我 http 和 https 有什么区别,我提了一点 https 多了一次 SSL 握手过程,紧接着就问我 http 和 https 三次握手(多亏了小林的计网模块)。 148 | 149 | ## Choose 150 | 151 | 怎么选?这个单拿出来讲。 152 | 153 | 从网上看到一个:钱多事少离家近,占两个就可以。 154 | 155 | 当你拿到 offer 之后,如何做抉择? 156 | 157 | 根据我询问各个大佬再加上个人见解,总结如下: 158 | 159 | **前景**,建议不要只看薪资,多看看企业目前发展状况,如果给你开高薪,而目前的发展前景已经不容乐观,那不就是 49 年入国军? 160 | 161 | **部门**,选择的部门打听一下是不是边缘部门,如果是边缘部门进去了,也很没意思。 162 | 163 | **岗位**,如果你是学后端的,结果给了你一个测开或者客户端的 offer,那最后还是要好好考虑一下,自己打听一下这些岗位的前景如何,再做决断。根据我询问的诸多大佬们,他们是建议能后端就不要选以上那俩,这也是我最终放弃其他的原因之一。 164 | 165 | **语言**,说实话这个其实无关紧要,不过还是得说一下,我是 Java 转 Go,如果是转小语种的话……建议好好想想以后,这语言的岗位公司需求多不多,别给自己挖坑。 166 | 167 | **城市**,这个非常重要,毕竟你要到一个人生地不熟且无亲无故,一切都要自己从头去打拼;既然选择了远方,就只顾风雨兼程。 168 | 169 | **镀金**,如果你和我一样,出身不好,那不如找个大厂镀金,干个几年再出去,也可乱杀。 170 | 171 | **薪资**,虽然没人能和钱过不去,不过眼光还是要放远,不要只看近三年能赚多少,看的远一点,一劳永逸。 172 | 173 | 最后,如果事情少不加班就更好了~ 174 | 175 | 摸鱼的快乐,难以形容。 176 | 177 | ## 感悟 178 | 179 | 在我做出最终的选择之后,也没再去投递了,说实话,累了。 180 | 181 | 既然选择了这条道路就不能后悔,既来之则安之。 182 | 183 | 没有哪家企业一开始就是大厂,一个地方正是因为不够好,所以才需要去建设,机遇就蕴含在其中。 184 | 185 | 之前在博客上看到一句话,这里贴出来给大家。 186 | 187 | > 加入大家口中的好公司,好部门而沾沾自喜,显得幼稚而虚荣。但凡思考过如何实现自我价值的互联网人应该都明白一点,一个已经成功的产品带来的荣誉感并不足以填补自己内心的不自信,也不利于提升自己;有能力的人最终会选择打造自己的产品,哪里能给最大的自由,能最大限度实现自己的想法,哪里就是最好的地方。 188 | > 189 | > 190 | > 191 | > 刚毕业的人喜欢谈论工资,为了一点月薪的差异破坏自己的心情,但到后面会发现,随着工资增长自己的力量并没有变大。最重要的,还是得升级角色,一个高阶角色,并不只代表着权力,更多的是责任和保护的能力。 192 | > 193 | > https://www.gonglin91.com/2018/04/18/beijing/ 194 | 195 | 196 | 197 | 我不相信宿命论,每个人都可以凭借自己的努力去实现梦想,一开始的我不相信,当它发生在我身上时,我信了,当时发誓,作为感激,我要保持学习的习惯。 198 | 199 | 另外建议大家少看一些负能量的东西,在脉脉上,我发现很多人都抱怨自己当前工作不顺利不满意进行宣泄,这些东西都会潜移默化的影响你,保持一颗积极乐观的心,做好本职工作就好了。 200 | 201 | 我是学一个东西很慢的人,**既然我都可以,你为什么不可以!** -------------------------------------------------------------------------------- /reader_nb/6_reader.md: -------------------------------------------------------------------------------- 1 | # 做了国外 CS 课程的 lab,拿到腾讯实习! 2 | 3 | 大家好,我是小林。 4 | 5 | 相信不少 CS 学生都有关于项目到底要怎么准备的问题,可能大家认为要做个非常强的项目才有机会面试。 6 | 7 | 在前几个星期,有位大三非科班的读者的项目经历写的是**国外 CS 课程的 lab**,也就是课程的实验,并不是什么高大上的项目,他依然拿到了腾讯的实习。 8 | 9 | ![图片](https://img-blog.csdnimg.cn/img_convert/d594992b15d3db17ee6cdb2804ab3176.png) 10 | 11 | 他跟说,对于校招面试,项目其实并不要求做的很牛逼,但是要保证是你自己亲手做的,因为面试时,问项目主要是问你在项目中用什么技术解决了什么问题,然后达到了什么效果,你能回答出这些才是主要的。 12 | 13 | 然后面试过程中,计算机基础和算法才是大头,考的最多的还是这些。 14 | 15 | 他由于在学校里参加过 ACM 比赛,所以面试时的算法对他不是难度,他甚至都没刷过 leetcode,但是每次面试的算法他都是秒杀的。 16 | 17 | 他的弱点主要是在计算机基础知识,因为他不是非科班的,很多计算机专业课都没上过,或者有的没怎么认真学过。 18 | 19 | 他最开始因为没有准备计算机基础,面试屡屡挫败,后面他开始突击一两月这些八股文,我的图解网络和系统也对他起到来一定的帮助,最终成功拿到腾讯的实习。 20 | 21 | 我也邀请这位读者分享他的学习经验和做 lab 的经验。 22 | 23 | 开车! 24 | 25 | ![图片](https://img-blog.csdnimg.cn/img_convert/2ef0e8ae154ff655c9b14c53a3e71a4f.png) 26 | 27 | ## 我跨专业是如何学 CS 的 28 | 29 | 简单概括我的情况。 30 | 31 | 我是名非科班的 ACM 选手,在准备面试之前这一段时间都没有系统的学习过专业课的知识 32 | 33 | 我是一名大三的机械系的学生,大概在大二上学期过完的假期,我刷知乎偶然看到了同学校学长描述自己的竞赛经历,觉得很好奇,就入坑了算法竞赛也就是俗称的 ACM,兴趣使然一直打到了大三下学期。 34 | 35 | 在这段时间里,我和队友或者其他计算机院的学生聊天的时候,偶尔会听到一些关于专业课的知识或者名词,然后会去搜这些相应的知识稍微进行了解,但是完全没有系统的学习过 CS 的专业课知识,基本上就是看到了什么知识点感觉好玩就去搜一搜。可以说,在面试之前我对 CS 的体系和知识是完全陌生的。 36 | 37 | 到了大三下学期,偶然看到学校群里有学姐发的实习招聘信息,我才意识到:我都大三了,是时候着手准备一下找工作的事了。 38 | 39 | 通过学姐发的招聘信息,我被内推去参加了 tx 的实习面试。 40 | 41 | 这一次面试的内容都十分简单,什么是多态,多态怎么实现的,进程和线程的内容是什么,计算机网络的几层结构是怎么样的……如果你有准备面试的话,你会发现这些问题简直就是送分题! 42 | 43 | 但由于我从来没有学习过相关的知识,被虐的惨不忍睹(面试官还和我说会进行评估,然后五分钟之后就把我挂了)才发现自己这一块有如此大的欠缺,开始着手一点点的从头开始补 CS 的知识。 44 | 45 | ## 如何学习专业课知识 46 | 47 | **不要上来就啃所谓的经典书。** 48 | 49 | 先去找一些经典的网课看看。对于新手来说,网课的老师可能讲的更像人话一点。 50 | 51 | 我个人主要推荐:**CSAPP**。从计算机的组成比如浮点数的存储方式,存储金字塔结构到操作系统的进程线程,计算机网络的 socket 等都有介绍,一个性价比很高的课程能够让你了解整个计算机体系结构。 52 | 53 | 其次我个人认为面试中常考察的点就是 **OS、计网、数据库和一些语言知识**。语言大家自己去找对应的课程学习。 54 | 55 | 计网的话我是看小林的图解网络 + b 站的湖科大的老师做的视频,都附带了很多图和动画,简单易懂。参考的文字材料就是自顶向下 + TCP/IP 详解卷 1。 56 | 57 | OS 没有找过网课看,觉得看完 CSAPP 之后挺好理解的了。用的文字材料主要是小林的图解系统 + 操作系统概论。 58 | 59 | 数据库网课看的是 15445,这是一个面向磁盘的数据库,后面发现大量的讲解的其实是 Mysql 的原理。然后文字材料参考的是 Mysql 技术内幕,Innodb 存储引擎和 Redis 设计与实现。 60 | 61 | 进阶的内容大家可以参照自己想要发展的方向学习啦,我认为学到这里的人对计算机体系结构有了了解之后,应该很容易找自己的方向。像我对分布式和数据库感兴趣,就选了 6.824 和 15445 作为扩展学习内容。 62 | 63 | 虽然我推荐了一些网课和书籍作为学习内容,但是我学习过程中绝对不止参考了这些东西。 64 | 65 | 对于一个知识点,我如果看不懂的话先考虑找公开课/博客,然后在书中找较为书面化的表达方式,最后**我还会在自己博客中写笔记,用自己的话表达也是一种学习方式**。 66 | 67 | ## 关于 6.824 和 15445 68 | 69 | 6.824 和 15445 这两个一个是关于分布式系统的。 70 | 71 | 他会讲一些关于 mapreduce 和 raft 的分布式算法,以及 15445,它一个基于磁盘的数据库(后来我看 innodb 引擎的时候发现其实就是对着 mysql 讲的)**选择这两个课程学习的好处在于他们的 lab 真的很好**,其实 CSAPP 出名的地方同样如此。 72 | 73 | 差不多的内容,讲师的水平肯定不会有决定性差距,差距主要体现在这些公开课由很好的 lab,带你手把手对一个知识进行实现算法/数据结构,让你对知识的理解更加深刻。 74 | 75 | 同时,由于这些是比较常见的算法,一般面试官都会懂,因此就喜欢和你聊一聊相关的问题,是一个面试的时候很好的谈资。你可以聊一聊你项目里面是怎么实现的,遇到了什么问题,如何解决的。 76 | 77 | 铺垫了这么多,如何学习这种公开课? 78 | 79 | 6.824 和 15445 在 b 站上都是有相应的视频的,但据我了解免费放出来的貌似都是机翻的,可能有一些地方语义不是很通畅,这里**我推荐一个组织叫 simviso,人工翻译公开课的视频,会比机翻的看起来舒服一些**。 80 | 81 | 然后关于怎么做公开课的 lab,其实你要实现的东西老师上课从答题思路到实现细节都会讲的很清楚,认真听了公开课之后你就知道里面的东西怎么运作的了,只需要自己再理一理怎么实现之类的问题就可以了。 82 | 83 | 同时,官网上也会有很详细的教程,告诉你要实现什么东西,然后每个模块你要实现什么样的函数,拥有什么样的功能,是**保姆级教学**!不用担心不会写的问题。 84 | 85 | 知识点在课上会说的很清楚,lab 做的东西绝对不会超纲,不仅课上会教你,官网的文档也写得很清楚,可能对于英语不是很好的小伙伴会造成一些困惑,但是我感觉当程序员面对英语文档应该会是很常见的事情。 86 | 87 | #### 6.824 lab 88 | 89 | 6.824 的官网地址: 90 | 91 | http://nil.csail.mit.edu/6.824/2018/schedule.html 92 | 93 | 6.824 的官网的 schedule,如下图: 94 | 95 | ![图片](https://img-blog.csdnimg.cn/img_convert/50567a129b0f94b7de9725dc6b6e4ef1.png) 96 | 97 | 下图是 lab1 的 mapreduce 介绍,这一部分是教你怎么用 git 配置环境的。 98 | 99 | ![图片](https://img-blog.csdnimg.cn/img_convert/158b732d6e6b3ff6c5aea1285b67f09a.png) 100 | 101 | 下图是 lab1 的 part1,告诉你你需要在 common_map.go 里实 现 domap()函数等等……会写的很清楚让你去实现哪一部分的模块。 102 | 103 | ![图片](https://img-blog.csdnimg.cn/img_convert/17ccd88cb4276063c2a85864441d7511.png) 104 | 105 | 而且,还提供了一些 test 来测试你实现的对不对,如下图: 106 | 107 | ![图片](https://img-blog.csdnimg.cn/img_convert/9d20734b607c90ae19a2ad2f46b7193d.png) 108 | 109 | #### 15445 lab 110 | 111 | 15445 的官网地址: 112 | 113 | https://15445.courses.cs.cmu.edu/fall2020/schedule.html 114 | 115 | 15445 的官网 schedule 如下图,可以找到里面的 project released 点进去。 116 | 117 | ![图片](https://img-blog.csdnimg.cn/img_convert/2bfa6458fbde31d203366e3bcc33a119.png) 118 | 119 | 下图是 lab1 的 LRU 模块,告诉你要去实现 src/……/lru_replacer.h 的 victim 函数啊,pin 函数啊等等,写的十分清楚。 120 | 121 | ![图片](https://img-blog.csdnimg.cn/img_convert/23da55e8b84a02ba7642bfe6ccc169e4.png) 122 | 123 | 所以,大家有时间的话,**一定要做 lab**。 124 | 125 | 我个人认为**国外的公开课的最大优势就是量身定制的 lab,真的能将你课上所学知识完美的再复现一遍,让你的理解更加深刻。** 126 | 127 | ## 参加竞赛对面试的帮助 128 | 129 | 这个问题可以转换为:**学算法有什么好处?** 130 | 131 | 其实面试中考察的算法,甚至是算法竞赛中考察的算法,都是很久以前计算机科学家们玩烂了的。在现实工程中有许多算法都是被淘汰了/用不上的。 132 | 133 | 那为什么还要学? 134 | 135 | 他们原本设计出来是为了解决某些特定问题的。比如最小生成树,原本就是为了解决计算机网络中的一些特定问题的,或者说二叉树,将二分这一个思想转换成了一个持久化的数据结构。 136 | 137 | 我个人的理解就是,**学习算法你可以学习原本解决这些计算机问题的思维,培养了计算机思维,在后续的专业课学习中就打了一个很好的基础。** 138 | 139 | 回到原问题,参加算法竞赛的好处在于奖项多了之后,简历更突出(帮助我一个双非的学生过了一些简历关),和具备扎实的编程功底,良好的计算机思维。 140 | 141 | 同时,参加了算法竞赛,基本上就是对面试的算法题进行了一个**降维打击**吧。 142 | 143 | **我虽然没有刷过 leetcode,但面试的算法题基本都没什么压力写出来了***(中间也挂了不少面试,但不是挂在算法上,是当时八股背的不好)。 144 | 145 | 虽然面试不仅仅是由算法题组成的,但对于很多同学来说,专业知识都掌握好了,但是死在了算法上,这就有点气人。还是得多多刷题~现在太卷啦,尽量都做到最好吧。 146 | 147 | ## 总结 148 | 149 | 在我面试的过程中,算法环节遇到的题目都是十分简单或者十分常见的问题,因此只要多刷刷题,提高自己的实现能力就 ok 了。 150 | 151 | **最主要的还是基础知识的准备**。 152 | 153 | 首先对照着一些简单易懂的公开课/小林的图解系列等了解知识雏形,然后再自己从专业书里更详细的学习。 154 | 155 | 同时自己写博客,多刷刷面经,看看面试喜欢考什么,大概就 ok 了? 156 | 157 | 也许吧,大家都要加油哦! -------------------------------------------------------------------------------- /reader_nb/7_reader.md: -------------------------------------------------------------------------------- 1 | ## 大三就啃框架源码!轻松因对字节面试 2 | 3 | 大家好,我是小林。 4 | 5 | 上周我发了个[读者字节三面的面经](https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247496990&idx=2&sn=160eaa432d4bfe7115fa7baee19db3ed&scene=21#wechat_redirect),结果评论区很多人不相信这是校招的面经,觉得难度有点高。 6 | 7 | ![图片](https://img-blog.csdnimg.cn/img_convert/8fe33baf189b2fb06919e79583abf93d.png) 8 | 9 | 首先这个确实是读者真实的校招面经,再来因为他之前实习过,他的实习项目里涉及到了不少中间件,所以面试官对于高并发问题考察比较多,也算是按简历来问的了。 10 | 11 | 正好他自己在学习的时候,**有看源码的习惯**,所以面试的问题,他都能应对,甚至能说到面试官眼前一亮,然后在几天拿到了字节 Java 后端的意向。 12 | 13 | 我也邀请了这位读者分享他的学习路线和心得,**他最后的心得讨论八股文的事情,说的很好,值得一读!** 14 | 15 | ------ 16 | 17 | ## 我的情况 18 | 19 | 我先说说我的基本情况。 20 | 21 | 双非一本,科班,大学 0 基础,随着学校课程,选择后端开发,大三寒假在某独角兽企业 Java 实习到提前批,字节已 oc。 22 | 23 | 我这次的分享更多的适合准备校招或者是大学已经决定选择后端方向的同学,社招大佬见笑~ 24 | 25 | 首先这里要说明一下哈,下面的路线可能对于很多人是「填鸭式教学」,就是基本会是面向面试准备的,关于面试工作实际开发以及程序员自我修养的这部分会放在最后心得部分,感兴趣的可以选择性观看。 26 | 27 | 我的观点始终是由点到线,由线再到面。 28 | 29 | 很多东西肯定是经典的书籍会更加权威和全面,但是说实话,不是每个人都能抱着本书看的进去,并且理解透彻,比如我一开始看书的时候就是很迷。 30 | 31 | 所以我的建议一直都是,**从弱智的,易懂的,有实际案例的博客或者是视频看起,了解大概,动动手撞撞坑之后,再去系统性的看书**,这样会比较好理解。 32 | 33 | 起码我是这样经历过来的,因为实在没有其他办法。 34 | 35 | ## 学习路线 36 | 37 | 首先推荐几个后端学习的仓库吧: 38 | 39 | Github 上很出名的: 40 | 41 | - javaguide 42 | - cs-note 43 | - JavaFamily 44 | 45 | 当然我也很期待小林大佬能早日推出自己的仓库啦~ 46 | 47 | 首先要搞清楚,作为后端开发,我们需要掌握一下什么知识点,其实无外乎就是如下内容。 48 | 49 | ### 编程语言 50 | 51 | 以 Java 为例子。 52 | 53 | #### Java 语法基础 54 | 55 | 这个没啥好说的,科班的话有教材,非科班的话建议看一下入门教程,菜鸟教程之类的(别嫌弱智、简单)。 56 | 57 | 看完之后可以全面的看一下进阶书籍(这是面向科班&非科班),例如著名的:Java 核心卷等(说实话,一开始让我看,真心看不下去,可能是我菜……)。 58 | 59 | #### 集合框架源码 60 | 61 | 必须是源码,现在这种烂八股应该不会有人不会源码吧?而且源码里面很多设计思想可以学到很多,例如 hash 的泊松分布推导等。 62 | 63 | 上面那部分基础看完应该都知道这是什么了吧……有能力有自信的可以根据面经题目自己去分析源码,小白同学建议网上找一些源码分析(这里一定要注意,我是很建议经常百度的,但是不要无脑百度,一个源码可以看多一些博客对比一下,然后自己实际去看一下分析一下是不是如此,切忌人云亦云),有一些能力之后自己去总结。 64 | 65 | #### 并发编程 66 | 67 | 并发编程是现在后端语言很重要的一块基础(当然工作中可能比较少机会可以用到),但是并发编程扎不扎实,运用的熟不熟练可以很好的体现一个程序员的编程素养和基本功。 68 | 69 | 这里肯定有大佬会吐槽说面试造航母,进门拧螺丝哈,hhh 确实并发编程其实一般场景运用不多,但是在项目里,很多可以优化的细节,别人做不到你能做到的,往往就是在这些点,包括下文 JVM。 70 | 71 | - 基础:上述的教程,自己打一下练练手。 72 | - 进阶:书籍《并发编程的艺术》、《并发编程实战》。 73 | 74 | #### JVM 虚拟机 75 | 76 | 这里可能很多人有些疑问了哈,实际开发中你写项目需要了解虚拟机吗?真正调优轮得到你吗? 77 | 78 | emmm,这块的话只能说见仁见智了,个人认为开发里的虚拟机小调试以及一些常见溢出的分析还是很有必要的。 79 | 80 | 退一万步说,学 Java 的,总不能连虚拟机都不知道吧。我个人认为啊,任何东西都是知其然、知其所以然、知其所以必然,了解清楚了,知道原理,你写代码的时候会有一种看透的感觉以及会有前所未有的安全感。 81 | 82 | - 基础:虚拟机有很多原理分析的整套文章,我这里就不推荐了,都可以搜的到。 83 | - 全面进阶:推荐周志明大佬的《深入 Java 虚拟机》,现在应该有第三版了。 84 | 85 | #### Java 相关框架使用以及源码了解 86 | 87 | 从常见的 ssm 到 spring 全家桶,这一块真心不好学,资料很杂,入门的话我建议去看一下黑马程序员,尚硅谷相关教程,或者是 how2j 这个网站,上面也有详细的路线和简要入门教程(各位大佬别吐槽哈,不是宣传培训班,但是确实作为小白入门很香)。 88 | 89 | 上面是简单入门,无脑运用,实际进阶的话还是看看源码看看书哈,一定要找些项目来做做。 90 | 91 | - 推荐书籍:《spring 源码分析》。 92 | - 面试重点:这块多去看面经和源码吧,实际开发里需要关注的点还是有的,例如异步注解的循环依赖报错,我在实习的时候就遇到了,不懂原理真心不好解决。 93 | 94 | ### 数据库 95 | 96 | 以 Mysql 为例子。 97 | 98 | 科班基础有教材,非科班基础推荐小林大佬的《图解 mysql》。 99 | 100 | 进阶的话,极客时间的《mysql45 讲》,书籍主要两本,分别是《高性能 mysql》和《mysql 技术内幕》,书籍建议有一定基础再看。 101 | 102 | 面试重点:小林&帅地的文章里都有很多,不再赘述。 103 | 104 | ### 中间件 105 | 106 | #### Redis 107 | 108 | 这个入门也是看一下教程会快一些,科班应该也没有专门的课程吧? 109 | 110 | 入门可以考虑尚硅谷之类的视频,进阶的话可以看看书和一些源码分析:《redis 的设计与实现》,老经典了吧。 111 | 112 | 不过 redis 个人认为难点在于运用,这点就不多说了,学完基础大家应该都会知道后续的学习路线了,可以期待一下小林大佬的《图解 redis》 113 | 114 | #### mq、kafka 115 | 116 | 这个有余力可以学一下,面向面试的话两者都行,实际运用看业务场景,这里不再赘述。 117 | 118 | 入门还是推荐看视频,b 站很多,随便搜,没有什么特别推荐。 119 | 120 | 进阶的话,推荐书籍《rocketmq 的技术内幕》,不过说实话,我自己也在看 mq 源码,感觉这本书写的一般,不够细节,目前没发现什么特别全面的书籍。 121 | 122 | ### 计算机基础 123 | 124 | #### 算法与数据结构 125 | 126 | 这一块的重要性不再多说啦,当然也有很多人吐槽这个没用的,确实我也很烦应试刷题,可以没办法,确实是面试硬性要求,尤其是外企入门推荐左程云大神视频&帅地玩编程的相关文章。 127 | 128 | #### 设计模式 129 | 130 | 科班有教材,非科班建议《大话设计模式》这类书籍入门 131 | 132 | 这一块偏抽象与实践,主要还是业务场景,需要自己多找找实际案例看看多理解。 133 | 134 | #### 计算机网络 135 | 136 | 科班有教材,非科班直接无脑小林大佬的《图解网络》。 137 | 138 | 看完之后进阶可以考虑详细的看一下《计算机自顶向下》,源码分析等,例如:开发者内功修炼公众号。 139 | 140 | 面试重点,后面心得会提到。 141 | 142 | #### 操作系统 143 | 144 | 科班有教材,非科班直接入手小林《图解系统》。 145 | 146 | 进阶可以考虑看一下《操作系统导论》,国外的课程 CSAPP,MIT 相关课程等等(动手会比较多)。 147 | 148 | 面试重点,后面心得会提到 149 | 150 | #### 汇编、计组 151 | 152 | 这一块科班应该很熟悉,噩梦,非科班的话建议找一些视频入手,b 站很多大学的计算机相关课程,播放量高的都挺不错。 153 | 154 | #### 分布式 rpc 155 | 156 | 这里不再赘述了,校招生估计大都是了解基本概念,相信学完上述内容,自己应该清楚该怎么学了~ 157 | 158 | #### 云原生云计算 159 | 160 | docker、k8s 这些这里不赘述了哈,相信需要学这个的大佬自己知道如何学了~ 161 | 162 | ## 一些心得 163 | 164 | 说实在的,刚刚那个路线其实稍微学过点后端的估计都知道,而且现在开源仓库太多啦,我想可能对大家帮助更大的还是自己的学习感悟吧。 165 | 166 | 其实说到这里相信大家肯定很清楚了,太多资料,太多八股了…… 167 | 168 | 这里聊一下我对八股的一些看法吧(因为我看到上次小林大佬发的面经下面评论有同学提到八股的问题)。 169 | 170 | 首先得搞清楚八股是什么,**一个知识点,你能把使用以及原理说出来,我称之为八股,但是你能把底层关联以及业务使用,优化历程也能搞清楚,我称之为能力**。 171 | 172 | 固然,现在的 CS 基本已经形成了套路,一套一套的面试题,很多人无脑跟着背就行,甚至现在还有分公司,分部门,对应的面经和知识点都有人总结,可见八股影响之深。 173 | 174 | 但是退一步说,**八股真的没用吗?八股不能体现你的能力吗?八股对于你的工作真的没有提升吗?** 175 | 176 | 以我自己为例吧,这里不以偏概全哈,单纯就是分享一下自己的经历和看法。 177 | 178 | ***例子一\*** 179 | 180 | 刚刚上文学习路线有说到虚拟机的学习,很多人吐槽是不是这玩意没必要学? 181 | 182 | 但其实呢,我在实习的时候就遇到了自己的一个模块线上 oom 了,排查了很久通过动态数据定位到是新生代与老年代比例的一个问题。 183 | 184 | 当然了,对于很多大佬来说,这不算什么。但是对于我来说,假设我连这一块的基本知识都没掌握的话,我即便百度到了解决办法(例如无脑把比例调大,把内存调大),还是没法从根本上解决问题,所以我还是挺庆幸的,不然那天就背锅了…… 185 | 186 | ***例子二\*** 187 | 188 | 再者说吧,还是八股的问题,面试的时候这可以作为你的一个优势,别人回答 CMS 就是简单的说下基本过程(我称之为八股),但是你回答可以把三色标记出现的问题以及 CMS 短暂的 STW 的问题引出 G1,并且还能举出例子,你在实习或者业务中使用的是什么收集器,为什么,怎么切换的(我称之为能力)。 189 | 190 | ***例子三\*** 191 | 192 | 再举个例子吧,一个很八股的问题,三次握手,老八股了。如果是简单的说出过程甚至需要说出中间的标志位,我都认为这是基础(八股),但是如果你能说出为什么前两次握手不能带数据,怎么避免攻击的,实际企业应用是怎么做的(开发者内功修炼里有相关文章),我称之为能力,我到现在还记得面试的时候把相关过程分析以及这一块内核的源码说出来的时候面试官惊讶的表情,这就不是八股了,这是你的优势。 193 | 194 | 同样的,工作中也是一样,我实习的部门是做底层开发的,网络嗅探,内核参数监控是常事,所以我会认为,作为一名程序员,不管说是不是真正用到了,但是实际上经常接触的东西,这些东西,还是值得多去了解的。 195 | 196 | 例如之前面经提到的 QUIC,实际上很多厂内部已经有类似协议开发的应用了,所以个人认为还算是一个很常见的东西。 197 | 198 | ***例子四\*** 199 | 200 | 多给一个例子吧,也是面试的小技巧,可以多说一点内容体现你的基本素养。 201 | 202 | 例如 Java 很熟悉的 volatile 关键字,假设说我是面试官,我的面试者只说到原子性有序性,JMM 内存模型这些,我会认为这是八股。 203 | 204 | 但是如果能说到汇编文件的 lock 前缀,内存屏障,MESI 设计,MESI 与 volatile 的关系,MESI 优化队列,总线锁与缓存锁,总线风暴,那么我认为这是能力。 205 | 206 | 至于这对工作有没有用,我个人认为,见仁见智吧(总线风暴就是一个点)。 207 | 208 | ***例子四\*** 209 | 210 | 上次面经文章分享的是我的实习项目,具体就不方便透露了,但是还是说一个点吧。 211 | 212 | 实习项目里使用了 mq,我在开发的时候会注意去看其他企业的相关实施方案,会去整理源码,然后开发过程中会进行压测,无意间我发现,我的 mq 使用的比隔壁一位高级开发还要溜,当然我就是随眼一瞟觉得他写的不怎么样。 213 | 214 | 那么我在面试的过程中,面试官同样的一个 mq 怎么保证消费可靠这种问题,我与别人回答的差距就出来了。 215 | 216 | 这里面我就不强调 os 和网络,数据库这些基础知识的重要性啦,我个人不是很建议出了什么新技术就莽着去学习,因为万物离不开底层,吃透底层,再搞清楚业务,那么现在的很多中间件啊,分布式相关的知识啊,个人认为只是新瓶装旧酒。 217 | 218 | 举上述例子我不是想秀自己的知识储备,这些在各位大佬面前真的是不值一提,羞愧万分,只是想通过个人在校招准备过程中说明一点。 219 | 220 | 就是**八股 ≠无用,面试通过 ≠ 背八股**。 221 | 222 | 很多时候你以为你问题都回答上来了,面试却挂了,面试官是在刷你 KPI(当然确实也有可能是)。 223 | 224 | 但是我更多的认为要多反思自己,是不是说到位了,是单纯的在背,还是说自己的理解,结合业务场景来说,并且说出优化的点,说出这种方法存在的问题,我个人觉得这是会让面试官眼前一亮的点。 225 | 226 | 同时,也是我在“八股”过程中的一点感悟吧,其实校招只是人生的一个阶段,欲速则不达,要珍惜现在能静下心来沉淀知识的时间。 227 | 228 | 程序员的内功修养与素养真的很重要,所以在“八股”之余会多去看看一些知识的源码以及企业里面的应用,会去看看《代码整洁之道》、《程序员的基本素养》等,总结成自己的笔记,未来希望能开源。 229 | 230 | 关于有人问到如何学的问题? 231 | 232 | 其实很简单,还是那句话,知识点都是那么多,深度和延伸得靠你自己了,一个大方向就是,从语言的实现到操作系统(网络)的实现,按照这个方向去搜集资料,去看源码,相信会很有收获的。 233 | 234 | 肯定有人提到,学这么多,进去还不是拧螺丝? 235 | 236 | 这个经典问题,只能说见仁见智了,我始终认为,有拧螺丝的时候,当然也有造航母的机会,这得看你的选择与把握机会的能力。 237 | 238 | 当然这里也要大厂小厂的争论,这里就不说了。不管在哪里,肯定会有拧螺丝的时候,但这不妨碍你有一颗自我学习,自己提升的心,与君共勉,这可能是我年轻气盛的想法。 239 | 240 | 最后再次说明一下,我是小林大佬的忠诚读者,他的图解系列给了我很多灵感和帮助,应他邀请写分享,第一次写这些类型的内容,如果有任何语言斟酌的不到位的话,望各位海涵! 241 | 242 | 最后如果大家能够从中有一点点的收获或者是认同,借小林大佬的光,万分荣幸,希望大家也能找到自己满意的工作。 -------------------------------------------------------------------------------- /reader_nb/8_reader.md: -------------------------------------------------------------------------------- 1 | # 文科生自学转码,成功拿下众多互联网大厂 offer! 2 | 3 | 前几天收到一个读者的喜讯。 4 | 5 | 他是一名文科生,不过他通过自学,在今年拿到了非常多的大厂实习 offer,岗位是前端开发。 6 | 7 | ![图片](https://img-blog.csdnimg.cn/img_convert/e8ccba9a38352f1fd0aa8e842e1cf233.png) 8 | 9 | 我觉得他很厉害,而且他转行经历值得有这方面想法的同学学习和参考,所以我就邀请他写了一个分享稿,希望对你们有帮助。 10 | 11 | ## 正文 12 | 13 | 我是来自某双一流高校的文科研一(保送本校),在今年 11 月份收到了阿里、腾讯、百度、字节、快手、滴滴、完美世界、商汤等几家厂的前端实习 offer,应该是投简历的公司都给了 offer。 14 | 15 | 我的前端学习过程大概持续了大概有一年,也就是从大四上学期快要结束的时候到目前。 16 | 17 | 我的学习方式也比较笨,最开始就是抱着大厚书肯。 18 | 19 | 《CSS 权威指南》(上下)、《Javascript 高级程序设计》(第四版)、《You don’t know JavaScript》、《Javascript 忍者秘籍》(第二版)这些就是我的入门书籍,这四本中前两本我都是看了两边,都在 1000 页左右,后面两本则是草草翻了一下。 20 | 21 | 这个过程为我打下了比较扎实的 JS 功底,大概是用了 2 个月的时候,我大概就能摸清楚原型/原型链、Promise/异步、闭包、Event loop 等 JS 中的一些核心知识点了。我觉得一开始看视频会好一点,我自己学习的时候看书看不懂的地方也是去 B 站看相关知识点的讲解。 22 | 23 | 紧接着的寒假,我就在家搞毕设,用 react 做了一个场外交易平台(导师做的方向偏向于行为经济学),使用 node、区块链和数据库搞了一个全栈的项目。 24 | 25 | 整个项目其实就是按照 B 站上的 React 目前播放量最高的那个视频(我看的时候刚出来没几天)学完之后写了一个应用,之后找了一些关于登陆注册、鉴权和状态管理的一些知识做了一些应用,整个项目就完善很多了(寒假剩余时间摸鱼)。 26 | 27 | 大四下学期,开始补计网和算法的基础知识(前端这边操作系统问的少一些)。 28 | 29 | 计网方面在 B 站看了中科大的 mooc,讲的不错,看了自顶向下方法那本书,**但是这些都不如小林哥的笔记比较好!!!**不是我吹,我字节一面完全背的小林哥的笔记,面试官直接感叹:“我面了这么多人,从没有一个人像你一样说的这么细致的。”(得益于大学文科背书功底?) 30 | 31 | 算法方面是看了《算法(第四版)》,youtube 上看的普林斯顿的网课,跟着写了点代码,然后这个学期剩余时间几乎都在谈恋爱。 32 | 33 | 接下来,就开始第一次面试。当时是陪对象去投春招,被 HR 拉着投了一个知名 K12 公司,当场被拉去面试,莫名其妙就过了。我看了一下名单上好像就一个人投了前端,好像那个人就在我前面,进去没多久就出来了,我自己却面了将近 3 个小时,写了 4 张 A4 纸正反向面。 34 | 35 | 暑期就去北京实习了,亲身感受了一层楼一夜之间被开除的感觉。我在北京实习的时候,每天上班地铁上背小林的笔记,周末去公司刷 leetcode,刷的方法就是按照题型刷一下。 36 | 37 | 实习归来感觉自己太菜了,好多技术栈都没学过。回来之后补了 linux 的一些东西,看了 docker,跟着 webpack 官网撸了一边,看了 koa2、redux、react-redux 源码,看了《狼书》(一二册)、《前端开发核心知识进阶》看了半本,再次去学习 JS 的相关基础知识。 38 | 39 | 11 月份的时候看了看,牛客上的面经,感觉自己好像也可以进字节了,就去面试了基本上每个厂都给了 offer,最后选择了去杭州阿里。 40 | 41 | 我个人的感觉,知识的进步就会经历「知道自己不会」到「不知道自己会」的这样一个过程,每天学习一点点,每个月都去看一本书或者看一个小的项目源码,切记闭门造车这种行为吧,很多时候自己学了很久的东西,可能就是项目源码里面的一个很浅显的东西或者是书上都写着的,看视频很多时候是一个入门的方式,看书和源码是比较好的深入的方式。 42 | 43 | 这段时间,好多次自我怀疑转行是否正确,能不能在秋招找到一个合适工作,我直到拿到快手的 offer 之前一直都觉得自己非常菜,快手的 HR 告诉我,“部门对你评价真的特别高,这边 Leader 专门跟我说一定要你来。” 44 | 45 | 慢慢的时间会见证我们一天天的变强的。 46 | 47 | ## 问答 48 | 49 | > 为什么要转互联网? 50 | 51 | 为什么转行,因为原来的专业不好就业,如果读博的话,老板虽然也是业内大牛,但是我对这个方向不感兴趣,而且有一个室友是信息竞赛保送上来,他做的是前端开发,当时感觉他正在从事的事情很酷,然后受影响就去做前端了。 52 | 53 | > 总共花了多长长时间学习? 54 | 55 | 总共学习的时间,除去整块玩的时间、准备毕设和修学分的时间,满打满算的话有 7 个月,老板不怎么 push,干什么也不管。 56 | 57 | > 刚开始接触编程的时候会不会觉得很困难,你又是怎么克服的? 58 | 59 | 刚开始的时候的确很难,但是我的大佬室友带着我飞,手把手教我 hello world,然后直到能到自己能写一些 demo 之后,感觉到成就感之后就更有动力。我觉得学习编程的前期找一个能问问题的老师真的很重要。 60 | 61 | > 你算法刷了多少题,你是怎么克服算法题的? 62 | 63 | 算法题一共刷了 300 道左右吧,暑假去实习的时候,我住在青旅里面,室友玩的很不错,我告诉他们,我每天晚上下班回来刷三道题,周末为了省钱周末去公司蹭饭刷一天算法(包三餐),不会写的背下来就好了,把主要思路背下来整理个笔记,可以跟别人交流一下整体思路。 64 | 65 | > 面试时,面试过会介意你文科的身份吗?是不是等有相关的互联网实习,再去面一线大厂会更容易? 66 | 67 | 面试官不太会介意出身,我觉得只要技能点点满了就好了,这对文科生能否通过简历关很重要,但是面试官还是会问问为什么转行,想好这个问题就好了(我就是实话实说)。 68 | 69 | 我觉得有第一份实习很重要,没有第一份实习很难找到后面的实习,我觉得我暑假的实习对于我下面的找工作有很大帮助。 70 | 71 | 暑假实习的第一天跟导师沟通的时候,我直接告诉我的导师我实习期间想要得到哪些成长: 72 | 73 | - 第一点:我希望能参与到一个 Vue 实际项目开发中; 74 | - 第二点:我想经历一个从需求评审到正式上线项目的完整过程; 75 | 76 | 当时,刚好我们组特别缺人,我基本上就是当正式工在用,这些需求都被满足了,这对我后面的成长帮助很大。但是第一份实习却很考验运气,很感谢上家公司给我 offer。 77 | 78 | ## 我的想法 79 | 80 | 以上就是这位文科生读者转行的心得分享了,接下里我说下我的感受。 81 | 82 | 我觉得这个读者很会抓住机会。 83 | 84 | 第一,他刚好有一个会前端的室友,抓住了一个被室友带飞的机会。这一点非常关键,因为单纯一个人学,没人交流会学的很乏味,而且很容易陷入困境。如果身边有一个可以随时交流的前端大佬,可以很快度过小白时期,有时候一个小小的问题,就能被一句话解决,而不是自己在网上折腾个几个小时。 85 | 86 | 第二,他很善于利用网上的免费资源,他看的视频和做的项目,大部分来自于 B 站上的视频。这就是互联网带给我们学习上的便利,但是再怎么便利,还是得自己去搜并且学起来才是真有用,而不是百度网盘下载了几十 G 的学习资源就等于会了。 87 | 88 | 第三,抓住了某 k12 公司的实习机会。他找的是前端开发,这个岗位相比后端开发没那么卷,而正好这家公司缺前端开发,于是就有了第一次在一家互联网方向的公司实习。有了这段实习经历后,对于他后面在面试一线大厂的时候,起到了很关键的作用,因为公司看到你有了一份实习经历,证明你自学的知识能实际投入到工作中,也就不会在意你是文科生这个身份,毕竟程序员是以技术能力说话的嘛。 89 | 90 | 今天分享就到这啦,我们下期见! -------------------------------------------------------------------------------- /reader_nb/README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | 本系列的文章是小林读者们互联网春招、秋招的真实经历,基本都是拿下互联网一线大厂 offer 的经验分享。 4 | 5 | 因为他们都说我的图解网络和图解系统对他们帮助很大,所以我也把他们的经历整理到了网站。 6 | 7 | 如果你正好是大学生,不妨可以看看他们的校招和学习经验的分享,或许能帮到你呢!:muscle: 8 | 9 | - [非科班本科拿下年薪 35w+ 的 offer](/reader_nb/1_reader.md) 10 | - [被字节捞了六七次,终于拿到 offer 了!](/reader_nb/2_reader.md) 11 | - [三本逆袭!拿到京东美团等 offer](/reader_nb/3_reader.md) 12 | - [拿下腾讯天美 offer 的春招经历](/reader_nb/4_reader.md) 13 | - [机械工作 2 年,自学转行进腾讯了!](/reader_nb/5_reader.md) 14 | - [做了国外 CS 课程的 lab,拿到腾讯实习!](/reader_nb/6_reader.md) 15 | - [大三就啃框架源码!轻松因对字节面试](/reader_nb/7_reader.md) 16 | - [文科生自学转码,成功拿下众多互联网大厂 offer!](/reader_nb/8_reader.md) 17 | 18 | ::: tip 19 | 如果你也因为小林的图解文章而拿到不错的 offer,可以向小林投稿你的经历,小林会给予你一定的稿费,同时还会收录到本站里,想投稿的同学可以加我的微信:xiaolincoding,期待你的分享。 20 | ::: 21 | -------------------------------------------------------------------------------- /redis/README.md: -------------------------------------------------------------------------------- 1 | # 图解 Redis 介绍 2 | 3 | 《图解 Redis》目前还在连载更新中,大家不要催啦:joy: ,更新完会第一时间整理 PDF 的。 4 | 5 | 目前已经更新好的文章: 6 | - **面试篇** :point_down: 7 | - [Redis 常见面试题](/redis/base/redis_interview.md) 8 | - **数据类型篇** :point_down: 9 | - [Redis 数据类型和应用场景](/redis/data_struct/command.md) 10 | - [图解 Redis 数据结构](/redis/data_struct/data_struct.md) 11 | - **持久化篇** :point_down: 12 | - [AOF 持久化是怎么实现的?](/redis/storage/aof.md) 13 | - [RDB 快照是怎么实现的?](/redis/storage/rdb.md) 14 | - [Redis 大 Key 对持久化有什么影响?](/redis/storage/bigkey_aof_rdb.md) 15 | - **功能篇**:point_down: 16 | - [Redis 过期删除策略和内存淘汰策略有什么区别?](/redis/module/strategy.md) 17 | - **高可用篇** :point_down: 18 | - [主从复制是怎么实现的?](/redis/cluster/master_slave_replication.md) 19 | - [为什么要有哨兵?](/redis/cluster/sentinel.html) 20 | - :joy: 正在赶稿的路上。。。。。 21 | - **缓存篇** :point_down: 22 | - [什么是缓存雪崩、击穿、穿透?](/redis/cluster/cache_problem.md) 23 | - [数据库和缓存如何保证一致性?](/redis/architecture/mysql_redis_consistency.md) 24 | 25 | ---- 26 | 27 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 28 | 29 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 30 | -------------------------------------------------------------------------------- /redis/cluster/cache_problem.md: -------------------------------------------------------------------------------- 1 | # 什么是缓存雪崩、击穿、穿透? 2 | 3 | 用户的数据一般都是存储于数据库,数据库的数据是落在磁盘上的,磁盘的读写速度可以说是计算机里最慢的硬件了。 4 | 5 | 当用户的请求,都访问数据库的话,请求数量一上来,数据库很容易就崩溃的了,所以为了避免用户直接访问数据库,会用 Redis 作为缓存层。 6 | 7 | 因为 Redis 是内存数据库,我们可以将数据库的数据缓存在 Redis 里,相当于数据缓存在内存,内存的读写速度比硬盘快好几个数量级,这样大大提高了系统性能。 8 | 9 | ![图片](https://img-blog.csdnimg.cn/img_convert/37e4378d2edcb5e217b00e5f12973efd.png) 10 | 11 | 引入了缓存层,就会有缓存异常的三个问题,分别是**缓存雪崩、缓存击穿、缓存穿透**。 12 | 13 | 这三个问题也是面试中很常考察的问题,我们不光要清楚地知道它们是怎么发生,还需要知道如何解决它们。 14 | 15 | 话不多说,**发车!** 16 | 17 | ![图片](https://img-blog.csdnimg.cn/img_convert/61781cd6d82e4a0cc5f7521333049f0d.png) 18 | 19 | ------ 20 | 21 | ## 缓存雪崩 22 | 23 | 通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。 24 | 25 | ![图片](https://img-blog.csdnimg.cn/img_convert/e2b8d2eb5536aa71664772457792ec40.png) 26 | 27 | 那么,当**大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机**时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是**缓存雪崩**的问题。 28 | 29 | ![图片](https://img-blog.csdnimg.cn/img_convert/717343a0da7a1b05edab1d1cdf8f28e5.png) 30 | 31 | 可以看到,发生缓存雪崩有两个原因: 32 | 33 | - 大量数据同时过期; 34 | - Redis 故障宕机; 35 | 36 | 不同的诱因,应对的策略也会不同。 37 | 38 | ### 大量数据同时过期 39 | 40 | 针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法有下面这几种: 41 | 42 | - 均匀设置过期时间; 43 | - 互斥锁; 44 | - 双 key 策略; 45 | - 后台更新缓存; 46 | 47 | *1. 均匀设置过期时间* 48 | 49 | 如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时,**给这些数据的过期时间加上一个随机数**,这样就保证数据不会在同一时间过期。 50 | 51 | *2. 互斥锁* 52 | 53 | 当业务线程在处理用户请求时,**如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存**(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。 54 | 55 | 实现互斥锁的时候,最好设置**超时时间**,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。 56 | 57 | *3. 双 key 策略* 58 | 59 | 我们对缓存数据可以使用两个 key,一个是**主 key,会设置过期时间**,一个是**备 key,不会设置过期**,它们只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。 60 | 61 | 当业务线程访问不到「主 key」的缓存数据时,就直接返回「备 key」的缓存数据,然后在更新缓存的时候,**同时更新「主 key」和「备 key」的数据。** 62 | 63 | 双 key 策略的好处是,当主 key 过期了,有大量请求获取缓存数据的时候,直接返回备 key 的数据,这样可以快速响应请求。而不用因为 key 失效而导致大量请求被锁阻塞住(采用了互斥锁,仅一个请求来构建缓存),后续再通知后台线程,重新构建主 key 的数据。 64 | 65 | *4. 后台更新缓存* 66 | 67 | 业务线程不再负责更新缓存,缓存也不设置有效期,而是**让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新**。 68 | 69 | 事实上,缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为**当系统内存紧张的时候,有些缓存数据会被“淘汰”**,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值,业务的视角就以为是数据丢失了。 70 | 71 | 解决上面的问题的方式有两种。 72 | 73 | 第一种方式,后台线程不仅负责定时更新缓存,而且也负责**频繁地检测缓存是否有效**,检测到缓存失效了,原因可能是系统紧张而被淘汰的,于是就要马上从数据库读取数据,并更新到缓存。 74 | 75 | 这种方式的检测时间间隔不能太长,太长也导致用户获取的数据是一个空值而不是真正的数据,所以检测的间隔最好是毫秒级的,但是总归是有个间隔时间,用户体验一般。 76 | 77 | 第二种方式,在业务线程发现缓存数据失效后(缓存数据被淘汰),**通过消息队列发送一条消息通知后台线程更新缓存**,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时,用户体验也比较好。 78 | 79 | 在业务刚上线的时候,我们最好提前把数据缓存起来,而不是等待用户访问才来触发缓存构建,这就是所谓的**缓存预热**,后台更新缓存的机制刚好也适合干这个事情。 80 | 81 | ### Redis 故障宕机 82 | 83 | 针对 Redis 故障宕机而引发的缓存雪崩问题,常见的应对方法有下面这几种: 84 | 85 | - 服务熔断或请求限流机制; 86 | - 构建 Redis 缓存高可靠集群; 87 | 88 | *1. 服务熔断或请求限流机制* 89 | 90 | 因为 Redis 故障宕机而导致缓存雪崩问题时,我们可以启动**服务熔断**机制,**暂停业务应用对缓存服务的访问,直接返回错误**,不用再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。 91 | 92 | 服务熔断机制是保护数据库的正常运行,但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作 93 | 94 | 为了减少对业务的影响,我们可以启用**请求限流**机制,**只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务**,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。 95 | 96 | *2. 构建 Redis 缓存高可靠集群* 97 | 98 | 服务熔断或请求限流机制是缓存雪崩发生后的应对方案,我们最好通过**主从节点的方式构建 Redis 缓存高可靠集群**。 99 | 100 | 如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。 101 | 102 | ------ 103 | 104 | ## 缓存击穿 105 | 106 | 我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频繁访问的数据被称为热点数据。 107 | 108 | 如果缓存中的**某个热点数据过期**了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是**缓存击穿**的问题。 109 | 110 | ![图片](https://img-blog.csdnimg.cn/img_convert/acb5f4e7ef24a524a53c39eb016f63d4.png) 111 | 112 | 可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。 113 | 114 | 应对缓存击穿可以采取前面说到两种方案: 115 | 116 | - 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。 117 | - 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间; 118 | 119 | ------ 120 | 121 | ## 缓存穿透 122 | 123 | 当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。 124 | 125 | 当用户访问的数据,**既不在缓存中,也不在数据库中**,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是**缓存穿透**的问题。 126 | 127 | ![图片](https://img-blog.csdnimg.cn/img_convert/b7031182f770a7a5b3c82eaf749f53b0.png) 128 | 129 | 缓存穿透的发生一般有这两种情况: 130 | 131 | - 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据; 132 | - 黑客恶意攻击,故意大量访问某些读取不存在数据的业务; 133 | 134 | 应对缓存穿透的方案,常见的方案有三种。 135 | 136 | - 第一种方案,非法请求的限制; 137 | - 第二种方案,缓存空值或者默认值; 138 | - 第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在; 139 | 140 | 第一种方案,非法请求的限制 141 | 142 | 当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断出请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。 143 | 144 | 第二种方案,缓存空值或者默认值 145 | 146 | 当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。 147 | 148 | *第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。* 149 | 150 | 我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。 151 | 152 | 即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。 153 | 154 | 那问题来了,布隆过滤器是如何工作的呢?接下来,我介绍下。 155 | 156 | 布隆过滤器由「初始值都为 0 的位图数组」和「N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。 157 | 158 | 布隆过滤器会通过 3 个操作完成标记: 159 | 160 | - 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值; 161 | - 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。 162 | - 第三步,将每个哈希值在位图数组的对应位置的值设置为 1; 163 | 164 | 举个例子,假设有一个位图数组长度为 8,哈希函数 3 个的布隆过滤器。 165 | 166 | ![图片](https://img-blog.csdnimg.cn/img_convert/86b0046c2622b2c4bda697f9bc0f5b28.png) 167 | 168 | 在数据库写入数据 x 后,把数据 x 标记在布隆过滤器时,数据 x 会被 3 个哈希函数分别计算出 3 个哈希值,然后在对这 3 个哈希值对 8 取模,假设取模的结果为 1、4、6,然后把位图数组的第 1、4、6 位置的值设置为 1。**当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中**。 169 | 170 | 布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时**存在哈希冲突的可能性**,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。 171 | 172 | 所以,**查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据**。 173 | 174 | ------ 175 | 176 | ## 总结 177 | 178 | 缓存异常会面临的三个问题:缓存雪崩、击穿和穿透。 179 | 180 | 其中,缓存雪崩和缓存击穿主要原因是数据不在缓存中,而导致大量请求访问了数据库,数据库压力骤增,容易引发一系列连锁反应,导致系统奔溃。不过,一旦数据被重新加载回缓存,应用又可以从缓存快速读取数据,不再继续访问数据库,数据库的压力也会瞬间降下来。因此,缓存雪崩和缓存击穿应对的方案比较类似。 181 | 182 | 而缓存穿透主要原因是数据既不在缓存也不在数据库中。因此,缓存穿透与缓存雪崩、击穿应对的方案不太一样。 183 | 184 | 我这里整理了表格,你可以从下面这张表格很好的知道缓存雪崩、击穿和穿透的区别以及应对方案。 185 | 186 | ![图片](https://img-blog.csdnimg.cn/img_convert/061e2c04e0ebca3425dd75dd035b6b7b.png) 187 | 188 | ------ 189 | 190 | 参考资料: 191 | 192 | 1.《极客时间:Redis 核心技术与实战》 193 | 194 | 2. https://github.com/doocs/advanced-java/blob/main/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md 195 | 3. https://medium.com/@mena.meseha/3-major-problems-and-solutions-in-the-cache-world-155ecae41d4f 196 | 197 | ---- 198 | 199 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 200 | 201 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 202 | -------------------------------------------------------------------------------- /redis/cluster/cluster.md: -------------------------------------------------------------------------------- 1 | # 为什么要有集群? 2 | 3 | 小林正在赶稿中。。。。。。 -------------------------------------------------------------------------------- /redis/storage/bigkey_aof_rdb.md: -------------------------------------------------------------------------------- 1 | # Redis 大 Key 对持久化有什么影响? 2 | 3 | 大家好,我是小林。 4 | 5 | 上周有位读者字节一二面时,被问到:**Redis 的大 Key 对持久化有什么影响?** 6 | 7 | ![·](https://img-blog.csdnimg.cn/2ae06f60d9614da0be4729944b2a317c.png) 8 | 9 | Redis 的持久化方式有两种:AOF 日志和 RDB 快照。 10 | 11 | 所以接下来,针对这两种持久化方式具体分析分析。 12 | 13 | ## 大 Key 对 AOF 日志的影响 14 | 15 | > 先说说 AOF 日志三种写回磁盘的策略 16 | 17 | Redis 提供了 3 种 AOF 日志写回硬盘的策略,分别是: 18 | - Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘; 19 | - Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘; 20 | - No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。 21 | 22 | 这三种策略只是在控制 fsync() 函数的调用时机。 23 | 24 | 当应用程序向文件写入数据时,内核通常先将数据复制到内核缓冲区中,然后排入队列,然后由内核决定何时写入硬盘。 25 | 26 | ![](https://img-blog.csdnimg.cn/def7d5328829470c9f3cfd15bbcc6814.png) 27 | 28 | 29 | 如果想要应用程序向文件写入数据后,能立马将数据同步到硬盘,就可以调用 fsync() 函数,这样内核就会将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。 30 | 31 | - Always 策略就是每次写入 AOF 文件数据后,就执行 fsync() 函数; 32 | - Everysec 策略就会创建一个异步任务来执行 fsync() 函数; 33 | - No 策略就是永不执行 fsync() 函数; 34 | 35 | 36 | > 分别说说这三种策略,在持久化大 Key 的时候,会影响什么? 37 | 38 | 在使用 Always 策略的时候,主线程在执行完命令后,会把数据写入到 AOF 日志文件,然后会调用 fsync() 函数,将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。 39 | 40 | **当使用 Always 策略的时候,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的**。 41 | 42 | 43 | 44 | 当使用 Everysec 策略的时候,由于是异步执行 fsync() 函数,所以大 Key 持久化的过程(数据同步磁盘)不会影响主线程。 45 | 46 | 当使用 No 策略的时候,由于永不执行 fsync() 函数,所以大 Key 持久化的过程不会影响主线程。 47 | 48 | 49 | ## 大 Key 对 AOF 重写和 RDB 的影响 50 | 51 | 当 AOF 日志写入了很多的大 Key,AOF 日志文件的大小会很大,那么很快就会触发 **AOF 重写机制**。 52 | 53 | AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 `fork()` 函数创建一个子进程来处理任务。 54 | 55 | 在创建子进程的过程中,操作系统会把父进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。 56 | 57 | ![](https://img-blog.csdnimg.cn/06657cb93ffa4a24b8fc5b3069cb29bf.png) 58 | 这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为**只读**。 59 | 60 | 随着 Redis 存在越来越多的大 Key,那么 Redis 就会占用很多内存,对应的页表就会越大。 61 | 62 | 在通过 `fork()` 函数创建子进程的时候,虽然不会复制父进程的物理内存,但是**内核会把父进程的页表复制一份给子进程,如果页表很大,那么这个复制过程是会很耗时的,那么在执行 fork 函数的时候就会发生阻塞现象**。 63 | 64 | 而且,fork 函数是由 Redis 主线程调用的,如果 fork 函数发生阻塞,那么意味着就会阻塞 Redis 主线程。由于 Redis 执行命令是在主线程处理的,所以当 Redis 主线程发生阻塞,就无法处理后续客户端发来的命令。 65 | 66 | 我们可以执行 `info` 命令获取到 latest_fork_usec 指标,表示 Redis 最近一次 fork 操作耗时。 67 | 68 | ```sql 69 | # 最近一次 fork 操作耗时 70 | latest_fork_usec:315 71 | ``` 72 | 如果 fork 耗时很大,比如超过 1 秒,则需要做出优化调整: 73 | - 单个实例的内存占用控制在 10 GB 以下,这样 fork 函数就能很快返回。 74 | - 如果 Redis 只是当作纯缓存使用,不关心 Redis 数据安全性问题,可以考虑关闭 AOF 和 AOF 重写,这样就不会调用 fork 函数了。 75 | - 在主从架构中,要适当调大 repl-backlog-size,避免因为 repl_backlog_buffer 不够大,导致主节点频繁地使用全量同步的方式,全量同步的时候,是会创建 RDB 文件的,也就是会调用 fork 函数。 76 | 77 | 78 | > 那什么时候会发生物理内存的复制呢? 79 | 80 | 81 | 当父进程或者子进程在向共享内存发起写操作时,CPU 就会触发**缺页中断**,这个缺页中断是由于违反权限导致的,然后操作系统会在「缺页异常处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「**写时复制 (Copy On Write)**」。 82 | 83 | ![](https://img-blog.csdnimg.cn/451024fe10374431aff6f93a8fed4638.png) 84 | 85 | 写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。 86 | 87 | 如果创建完子进程后,**父进程对共享内存中的大 Key 进行了修改,那么内核就会发生写时复制,会把物理内存复制一份,由于大 Key 占用的物理内存是比较大的,那么在复制物理内存这一过程中,也是比较耗时的,于是父进程(主线程)就会发生阻塞**。 88 | 89 | 所以,有两个阶段会导致阻塞父进程: 90 | 91 | - 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长; 92 | - 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长; 93 | 94 | 这里额外提一下,如果 **Linux 开启了内存大页,会影响 Redis 的性能的**。 95 | 96 | Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的。 97 | 98 | 99 | 如果采用了内存大页,那么即使客户端请求只修改 100B 的数据,在发生写时复制后,Redis 也需要拷贝 2MB 的大页。相反,如果是常规内存页机制,只用拷贝 4KB。 100 | 101 | 两者相比,你可以看到,每次写命令引起的**复制内存页单位放大了 512 倍,会拖慢写操作的执行时间,最终导致 Redis 性能变慢**。 102 | 103 | 那该怎么办呢?很简单,关闭内存大页(默认是关闭的)。 104 | 105 | 禁用方法如下: 106 | 107 | ```shell 108 | echo never > /sys/kernel/mm/transparent_hugepage/enabled 109 | ``` 110 | 111 | 112 | ## 总结 113 | 114 | 当 AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。 115 | 116 | 117 | AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 `fork()` 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程): 118 | 119 | - 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长; 120 | - 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。 121 | 122 | 大 key 除了会影响持久化之外,还会有以下的影响。 123 | 124 | - 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。 125 | 126 | - 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。 127 | 128 | - 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。 129 | 130 | - 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。 131 | 132 | 如何避免大 Key 呢? 133 | 134 | 最好在设计阶段,就把大 key 拆分成一个一个小 key。或者,定时检查 Redis 是否存在大 key,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程。 135 | 136 | 完! 137 | 138 | --- 139 | 140 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 141 | 142 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) -------------------------------------------------------------------------------- /redis/storage/rdb.md: -------------------------------------------------------------------------------- 1 | # RDB 快照是怎么实现的? 2 | 3 | 大家好,我是小林哥。 4 | 5 | 虽说 Redis 是内存数据库,但是它为数据的持久化提供了两个技术。 6 | 7 | 分别是「AOF 日志和 RDB 快照」。 8 | 9 | 这两种技术都会用各用一个日志文件来记录信息,但是记录的内容是不同的。 10 | 11 | - AOF 文件的内容是操作命令; 12 | - RDB 文件的内容是二进制数据。 13 | 14 | 关于 AOF 持久化的原理我在上一篇已经介绍了,今天主要讲下 **RDB 快照**。 15 | 16 | 所谓的快照,就是记录某一个瞬间东西,比如当我们给风景拍照时,那一个瞬间的画面和信息就记录到了一张照片。 17 | 18 | 所以,RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。 19 | 20 | 因此在 Redis 恢复数据时,RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。 21 | 22 | 接下来,就来具体聊聊 RDB 快照。 23 | 24 | ## 快照怎么用? 25 | 26 | 要熟悉一个东西,先看看怎么用是比较好的方式。 27 | 28 | Redis 提供了两个命令来生成 RDB 文件,分别是 `save` 和 `bgsave`,他们的区别就在于是否在「主线程」里执行: 29 | 30 | - 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,**会阻塞主线程**; 31 | - 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以**避免主线程的阻塞**; 32 | 33 | RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。 34 | 35 | Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,默认会提供以下配置: 36 | 37 | ```plain 38 | save 900 1 39 | save 300 10 40 | save 60 10000 41 | ``` 42 | 43 | 别看选项名叫 save,实际上执行的是 bgsave 命令,也就是会创建子进程来生成 RDB 快照文件。 44 | 45 | 只要满足上面条件的任意一个,就会执行 bgsave,它们的意思分别是: 46 | 47 | - 900 秒之内,对数据库进行了至少 1 次修改; 48 | - 300 秒之内,对数据库进行了至少 10 次修改; 49 | - 60 秒之内,对数据库进行了至少 10000 次修改。 50 | 51 | 这里提一点,Redis 的快照是**全量快照**,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。 52 | 53 | 所以可以认为,执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。 54 | 55 | 通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据。 56 | 57 | 这就是 RDB 快照的缺点,在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少。 58 | 59 | ## 执行快照时,数据能被修改吗? 60 | 61 | 那问题来了,执行 bgsave 过程中,由于是交给子进程来构建 RDB 文件,主线程还是可以继续工作的,此时主线程可以修改数据吗? 62 | 63 | 如果不可以修改数据的话,那这样性能一下就降低了很多。如果可以修改数据,又是如何做到到呢? 64 | 65 | 直接说结论吧,执行 bgsave 过程中,Redis 依然**可以继续处理操作命令**的,也就是数据是能被修改的。 66 | 67 | 那具体如何做到到呢?关键的技术就在于**写时复制技术(Copy-On-Write, COW)。** 68 | 69 | 执行 bgsave 命令的时候,会通过 `fork()` 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个。 70 | 71 | ![图片](https://img-blog.csdnimg.cn/img_convert/c34a9d1f58d602ff1fe8601f7270baa7.png) 72 | 73 | 只有在发生修改内存数据的情况时,物理内存才会被复制一份。 74 | 75 | ![图片](https://img-blog.csdnimg.cn/img_convert/ebd620db8a1af66fbeb8f4d4ef6adc68.png) 76 | 77 | 这样的目的是为了减少创建子进程时的性能损耗,从而加快创建子进程的速度,毕竟创建子进程的过程中,是会阻塞主线程的。 78 | 79 | 所以,创建 bgsave 子进程后,由于共享父进程的所有内存数据,于是就可以直接读取主线程(父进程)里的内存数据,并将数据写入到 RDB 文件。 80 | 81 | 当主线程(父进程)对这些共享的内存数据也都是只读操作,那么,主线程(父进程)和 bgsave 子进程相互不影响。 82 | 83 | 但是,如果主线程(父进程)要**修改共享数据里的某一块数据**(比如键值对 `A`)时,就会发生写时复制,于是这块数据的**物理内存就会被复制一份(键值对 `A'`)**,然后**主线程在这个数据副本(键值对 `A'`)进行修改操作**。与此同时,**bgsave 子进程可以继续把原来的数据(键值对 `A`)写入到 RDB 文件**。 84 | 85 | 就是这样,Redis 使用 bgsave 对当前内存中的所有数据做快照,这个操作是由 bgsave 子进程在后台完成的,执行时不会阻塞主线程,这就使得主线程同时可以修改数据。 86 | 87 | 细心的同学,肯定发现了,bgsave 快照过程中,如果主线程修改了共享数据,**发生了写时复制后,RDB 快照保存的是原本的内存数据**,而主线程刚修改的数据,是没办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照。 88 | 89 | 所以 Redis 在使用 bgsave 快照过程中,如果主线程修改了内存数据,不管是否是共享的内存数据,RDB 快照都无法写入主线程刚修改的数据,因为此时主线程(父进程)的内存数据和子进程的内存数据已经分离了,子进程写入到 RDB 文件的内存数据只能是原本的内存数据。 90 | 91 | 如果系统恰好在 RDB 快照文件创建完毕后崩溃了,那么 Redis 将会丢失主线程在快照期间修改的数据。 92 | 93 | 另外,写时复制的时候会出现这么个极端的情况。 94 | 95 | 在 Redis 执行 RDB 持久化期间,刚 fork 时,主进程和子进程共享同一物理内存,但是途中主进程处理了写操作,修改了共享内存,于是当前被修改的数据的物理内存就会被复制一份。 96 | 97 | 那么极端情况下,**如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍。** 98 | 99 | 所以,针对写操作多的场景,我们要留意下快照过程中内存的变化,防止内存被占满了。 100 | 101 | ## RDB 和 AOF 合体 102 | 103 | 尽管 RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握: 104 | 105 | - 如果频率太低,两次快照间一旦服务器发生宕机,就可能会丢失比较多的数据; 106 | - 如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销。 107 | 108 | 那有没有什么方法不仅有 RDB 恢复速度快的优点和,又有 AOF 丢失数据少的优点呢? 109 | 110 | 当然有,那就是将 RDB 和 AOF 合体使用,这个方法是在 Redis 4.0 提出的,该方法叫**混合使用 AOF 日志和内存快照**,也叫混合持久化。 111 | 112 | 如果想要开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成 yes: 113 | 114 | ```plain 115 | aof-use-rdb-preamble yes 116 | ``` 117 | 118 | 混合持久化工作在 **AOF 日志重写过程**。 119 | 120 | 当开启了混合持久化时,在 AOF 重写日志时,`fork` 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。 121 | 122 | 也就是说,使用了混合持久化,AOF 文件的**前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据**。 123 | 124 | ![图片](https://img-blog.csdnimg.cn/img_convert/f67379b60d151262753fec3b817b8617.png) 125 | 126 | 这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样**加载的时候速度会很快**。 127 | 128 | 加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得**数据更少的丢失**。 129 | 130 | ---- 131 | 132 | 最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。 133 | 134 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E5%85%B6%E4%BB%96/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BB%8B%E7%BB%8D.png) 135 | 136 | --------------------------------------------------------------------------------