diff --git a/images/1568024209257.png b/images/20190909-threads.png similarity index 100% rename from images/1568024209257.png rename to images/20190909-threads.png diff --git a/images/20191218105106.png b/images/20191217-01pprof.png similarity index 100% rename from images/20191218105106.png rename to images/20191217-01pprof.png diff --git a/images/20191222180715.png b/images/20191222-01http-pool.png similarity index 100% rename from images/20191222180715.png rename to images/20191222-01http-pool.png diff --git a/images/image-20191222135530268.png b/images/20191222-01segment-stack.png similarity index 100% rename from images/image-20191222135530268.png rename to images/20191222-01segment-stack.png diff --git a/images/image-20191222135826148.png b/images/20191222-02continuous-stack.png similarity index 100% rename from images/image-20191222135826148.png rename to images/20191222-02continuous-stack.png diff --git a/images/20200109-golang-gc-deep-01.jpg b/images/20200109-golang-gc-deep-01.jpg new file mode 100644 index 0000000..ce54e84 Binary files /dev/null and b/images/20200109-golang-gc-deep-01.jpg differ diff --git a/images/20200109-golang-gc-deep-02.jpg b/images/20200109-golang-gc-deep-02.jpg new file mode 100644 index 0000000..04d67df Binary files /dev/null and b/images/20200109-golang-gc-deep-02.jpg differ diff --git a/images/20200109-golang-gc-deep-03.jpg b/images/20200109-golang-gc-deep-03.jpg new file mode 100644 index 0000000..4faab25 Binary files /dev/null and b/images/20200109-golang-gc-deep-03.jpg differ diff --git a/images/20200109-golang-gc-deep-04.jpg b/images/20200109-golang-gc-deep-04.jpg new file mode 100644 index 0000000..60c8613 Binary files /dev/null and b/images/20200109-golang-gc-deep-04.jpg differ diff --git a/images/20200109-golang-gc-deep-05.jpg b/images/20200109-golang-gc-deep-05.jpg new file mode 100644 index 0000000..d515f34 Binary files /dev/null and b/images/20200109-golang-gc-deep-05.jpg differ diff --git a/images/20200109-golang-gc-deep-06.jpg b/images/20200109-golang-gc-deep-06.jpg new file mode 100644 index 0000000..101ee0b Binary files /dev/null and b/images/20200109-golang-gc-deep-06.jpg differ diff --git a/images/20200109-golang-gc-deep-07.jpg b/images/20200109-golang-gc-deep-07.jpg new file mode 100644 index 0000000..04d67df Binary files /dev/null and b/images/20200109-golang-gc-deep-07.jpg differ diff --git a/images/20200109-golang-gc-deep-08.jpg b/images/20200109-golang-gc-deep-08.jpg new file mode 100644 index 0000000..294e913 Binary files /dev/null and b/images/20200109-golang-gc-deep-08.jpg differ diff --git a/images/20200109-golang-gc-deep-09.jpg b/images/20200109-golang-gc-deep-09.jpg new file mode 100644 index 0000000..d22e009 Binary files /dev/null and b/images/20200109-golang-gc-deep-09.jpg differ diff --git a/images/20200109-golang-gc-deep-10.jpg b/images/20200109-golang-gc-deep-10.jpg new file mode 100644 index 0000000..155fb0f Binary files /dev/null and b/images/20200109-golang-gc-deep-10.jpg differ diff --git a/images/20200109-golang-gc-deep-11.jpg b/images/20200109-golang-gc-deep-11.jpg new file mode 100644 index 0000000..fa7805f Binary files /dev/null and b/images/20200109-golang-gc-deep-11.jpg differ diff --git a/images/20200109-golang-gc-deep-12.jpg b/images/20200109-golang-gc-deep-12.jpg new file mode 100644 index 0000000..dab219b Binary files /dev/null and b/images/20200109-golang-gc-deep-12.jpg differ diff --git a/images/20200109-golang-gc-deep-13.jpg b/images/20200109-golang-gc-deep-13.jpg new file mode 100644 index 0000000..40921ae Binary files /dev/null and b/images/20200109-golang-gc-deep-13.jpg differ diff --git a/images/20200109-golang-gc-deep-14.jpg b/images/20200109-golang-gc-deep-14.jpg new file mode 100644 index 0000000..6f7076a Binary files /dev/null and b/images/20200109-golang-gc-deep-14.jpg differ diff --git a/images/20200109-golang-gc-deep-15.jpg b/images/20200109-golang-gc-deep-15.jpg new file mode 100644 index 0000000..28f91e5 Binary files /dev/null and b/images/20200109-golang-gc-deep-15.jpg differ diff --git a/images/20200109-golang-gc-deep-16.jpg b/images/20200109-golang-gc-deep-16.jpg new file mode 100644 index 0000000..4e0a06b Binary files /dev/null and b/images/20200109-golang-gc-deep-16.jpg differ diff --git a/images/20200109-golang-gc-deep-17.jpg b/images/20200109-golang-gc-deep-17.jpg new file mode 100644 index 0000000..60c8613 Binary files /dev/null and b/images/20200109-golang-gc-deep-17.jpg differ diff --git a/images/20200109-golang-gc-deep-18.jpg b/images/20200109-golang-gc-deep-18.jpg new file mode 100644 index 0000000..f9ad36f Binary files /dev/null and b/images/20200109-golang-gc-deep-18.jpg differ diff --git a/images/20200109-golang-gc-deep-19.jpg b/images/20200109-golang-gc-deep-19.jpg new file mode 100644 index 0000000..101ee0b Binary files /dev/null and b/images/20200109-golang-gc-deep-19.jpg differ diff --git a/images/20200119-nginx_api.jpg b/images/20200119-nginx_api.jpg new file mode 100644 index 0000000..bd46145 Binary files /dev/null and b/images/20200119-nginx_api.jpg differ diff --git a/images/20200318mysql_redo.png b/images/20200318mysql_redo.png new file mode 100644 index 0000000..4c0505c Binary files /dev/null and b/images/20200318mysql_redo.png differ diff --git a/images/20201028-influx.jpg b/images/20201028-influx.jpg new file mode 100644 index 0000000..7ac70e5 Binary files /dev/null and b/images/20201028-influx.jpg differ diff --git a/images/20201210-callstack.png b/images/20201210-callstack.png new file mode 100644 index 0000000..72fe7e4 Binary files /dev/null and b/images/20201210-callstack.png differ diff --git a/images/2021/20210716-01connect.png b/images/2021/20210716-01connect.png new file mode 100644 index 0000000..2da5651 Binary files /dev/null and b/images/2021/20210716-01connect.png differ diff --git a/images/2021/20211228-01-env.png b/images/2021/20211228-01-env.png new file mode 100644 index 0000000..87951a1 Binary files /dev/null and b/images/2021/20211228-01-env.png differ diff --git a/images/2021/20211228-02-arch.png b/images/2021/20211228-02-arch.png new file mode 100644 index 0000000..8d2093a Binary files /dev/null and b/images/2021/20211228-02-arch.png differ diff --git a/images/2021/20211228-03-iptables-exec.png b/images/2021/20211228-03-iptables-exec.png new file mode 100644 index 0000000..5dfdb15 Binary files /dev/null and b/images/2021/20211228-03-iptables-exec.png differ diff --git a/images/2021/20211228-04-iptables-docker.png b/images/2021/20211228-04-iptables-docker.png new file mode 100644 index 0000000..2efda07 Binary files /dev/null and b/images/2021/20211228-04-iptables-docker.png differ diff --git a/images/2021/20211228-05-iptables-host.png b/images/2021/20211228-05-iptables-host.png new file mode 100644 index 0000000..47687e2 Binary files /dev/null and b/images/2021/20211228-05-iptables-host.png differ diff --git a/images/20210219-01wlxy.jpg b/images/20210219-01wlxy.jpg new file mode 100644 index 0000000..5c8a47e Binary files /dev/null and b/images/20210219-01wlxy.jpg differ diff --git a/images/20210219-02tcp_3.jpg b/images/20210219-02tcp_3.jpg new file mode 100644 index 0000000..56c7b0c Binary files /dev/null and b/images/20210219-02tcp_3.jpg differ diff --git a/images/20210219-03tcp_4.jpg b/images/20210219-03tcp_4.jpg new file mode 100644 index 0000000..979687b Binary files /dev/null and b/images/20210219-03tcp_4.jpg differ diff --git a/images/20210219-04dns.jpg b/images/20210219-04dns.jpg new file mode 100644 index 0000000..d131196 Binary files /dev/null and b/images/20210219-04dns.jpg differ diff --git a/images/20210618-tcp-congestion.jpg b/images/20210618-tcp-congestion.jpg new file mode 100644 index 0000000..d61fd47 Binary files /dev/null and b/images/20210618-tcp-congestion.jpg differ diff --git a/images/20210618-tcp-sliding-window.webp b/images/20210618-tcp-sliding-window.webp new file mode 100644 index 0000000..2aabaec Binary files /dev/null and b/images/20210618-tcp-sliding-window.webp differ diff --git a/images/20210620-congestion.png b/images/20210620-congestion.png new file mode 100644 index 0000000..eafcf68 Binary files /dev/null and b/images/20210620-congestion.png differ diff --git a/images/20210620-osi.png b/images/20210620-osi.png new file mode 100644 index 0000000..658a644 Binary files /dev/null and b/images/20210620-osi.png differ diff --git a/images/20210620-tcp.png b/images/20210620-tcp.png new file mode 100644 index 0000000..dfab41a Binary files /dev/null and b/images/20210620-tcp.png differ diff --git "a/markdown/2019/20190909go\347\232\204\345\215\217\347\250\213\350\260\203\345\272\246\346\225\264\347\220\206.md" "b/markdown/2019/20190909go\347\232\204\345\215\217\347\250\213\350\260\203\345\272\246\346\225\264\347\220\206.md" index 26b287d..2d28cbe 100644 --- "a/markdown/2019/20190909go\347\232\204\345\215\217\347\250\213\350\260\203\345\272\246\346\225\264\347\220\206.md" +++ "b/markdown/2019/20190909go\347\232\204\345\215\217\347\250\213\350\260\203\345\272\246\346\225\264\347\220\206.md" @@ -25,7 +25,7 @@ go的协程调度整理 三个线程模型图例:
- +
(2). 内核调度 diff --git "a/markdown/2019/20191217golang_pprof\344\275\277\347\224\250.md" "b/markdown/2019/20191217golang_pprof\344\275\277\347\224\250.md" index 4d644c0..57f0e96 100644 --- "a/markdown/2019/20191217golang_pprof\344\275\277\347\224\250.md" +++ "b/markdown/2019/20191217golang_pprof\344\275\277\347\224\250.md" @@ -9,13 +9,9 @@ pprof是golang对于runtime运行时进行系统状态监控的工具。golang (1).封装pprof类库,参见[go-libs/pprof](https://github.com/alwaysthanks/learning-docs/blob/master/go-libs/pprof/pprof.go) ``` -//cpu采样函数 -StartCpuProf() -StopCpuProf() -//内存采样 -SaveMemProf() -//goroutine数量采样 -SaveGoroutineProfile() +
+ +
``` (2).嵌入代码用例等进行采样 @@ -131,7 +127,7 @@ X轴代表采样总量。从左到右并不代表时间变化,从左到右也 - `alloc_space`用于查看堆内存的累计分配,通过`web`查看内存堆积处
- +
diff --git "a/markdown/2019/20191222golang\344\271\213http\350\277\236\346\216\245\346\261\240.md" "b/markdown/2019/20191222golang\344\271\213http\350\277\236\346\216\245\346\261\240.md" index fdfb133..ac711ff 100644 --- "a/markdown/2019/20191222golang\344\271\213http\350\277\236\346\216\245\346\261\240.md" +++ "b/markdown/2019/20191222golang\344\271\213http\350\277\236\346\216\245\346\261\240.md" @@ -9,9 +9,8 @@ 在http早期,每个http请求都要求打开一个tpc socket连接,并且使用一次之后就断开这个tcp连接。 使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,可以减少tcp连接建立次数,也意味着可以减少TIME_WAIT状态连接,以此提高性能和提高httpd服务器的吞吐率(更少的tcp连接意味着更少的系统内核调用,socket的accept()和close()调用)。
- +
- 在HTTP/1.0,为了实现client到web-server能支持长连接,必须在HTTP请求头里显示指定: `Connection:keep-alive` 在HTTP/1.1,就默认是开启了keep-alive,要关闭keep-alive需要在HTTP请求头里显示指定: diff --git "a/markdown/2019/20191222golang\344\271\213\346\240\210.md" "b/markdown/2019/20191222golang\344\271\213\346\240\210.md" index 39a761b..2cf3824 100644 --- "a/markdown/2019/20191222golang\344\271\213\346\240\210.md" +++ "b/markdown/2019/20191222golang\344\271\213\346\240\210.md" @@ -23,9 +23,10 @@ ##### (1)分段栈
- +
+ 对于分段栈(Segmented stacks), 如上图,当G调用H的时候,没有足够的栈空间来让H运行,这时候Go运行环境就会从堆里分配一个新的栈内存块去让H运行。在H返回到G之前,新分配的内存块被释放回堆。这种管理栈的方法一般都工作得很好。但对有些代码,特别是递归调用,它会造成程序不停地分配和释放新的内存空间。举个例子,在一个程序里,函数G会在一个循环里调用很多次H函数。每次调用都会分配一块新的内存空间。这就是热分裂问题(hot split problem)。 - 优点:动态扩展,初始成本小,可以将协程当作廉价资源使用。 @@ -34,9 +35,10 @@ ##### (2)连续栈
- +
+ - 优点:动态扩展,初始成本小,可以将协程当作廉价资源使用,且不存在hot split problem问题 - 缺点:由于通常以2倍扩展,当请求量密集,内存敏感的情况下,内存会消耗比较多,容易oom,当然,通常的业务量是ok的,不会有任何问题。同时100w连接才要考虑优化。 diff --git "a/markdown/2020/20200108-golang\344\271\213gc\345\205\245\351\227\250\347\257\207.md" "b/markdown/2020/20200108-golang\344\271\213gc\345\205\245\351\227\250\347\257\207.md" index a581314..feeb531 100644 --- "a/markdown/2020/20200108-golang\344\271\213gc\345\205\245\351\227\250\347\257\207.md" +++ "b/markdown/2020/20200108-golang\344\271\213gc\345\205\245\351\227\250\347\257\207.md" @@ -42,7 +42,7 @@ Golang GC系列共分为三篇. 分别为: ### 1.GC相关概念 -#### GC简介 +#### 1.1 GC简介 垃圾回收(英语:Garbage Collection,缩写为GC)是一种自动内存管理机制. 垃圾回收器(Garbage Collector)尝试回收不再被程序所需要的对象所占用的内存. GC最早起源于LISP语言, 1959年左右由John McCarthy创造用以简化LISP中的内存管理. 所以GC可以说是一项"古老"的技术, 但是直到20世纪90年代Java的出现并流行, 广大的普通程序员们才得以接触GC. 当前许多语言如Go, Java, C#, JS和Python等都支持GC. @@ -125,7 +125,7 @@ func ReturnValue() *Person { GC大大减少了开发者编码时的心智负担, 把精力集中在更本质的编程工作上, 同时也大大减少了程序的错误. -#### GC与资源回收 +#### 1.2 GC与资源回收 **GC是一种内存管理机制. 在有GC的语言中也要注意释放文件, 连接, 数据库等资源!** @@ -135,7 +135,7 @@ GC大大减少了开发者编码时的心智负担, 把精力集中在更本质 Java和Go均有类似的机制, 目前Java 1.9中已经明确把finalizer标记为废弃. -#### 术语简单说明 +#### 1.3 术语简单说明 这里简单的说明一些术语,帮助快速了解, 并不追求完全准确. @@ -167,7 +167,7 @@ stop the world, GC的一些阶段需要停止所有的mutator(应用代码)以 如果某一个对象在程序的后续执行中可能会被mutator访问, 则称该对象是存活的, 不存活的对象就是我们所说的garbage. 一般通过可达性来表示存活性. -#### Mark Sweep +#### 1.4 Mark Sweep 三大GC基础算法中的一种. 分为mark(标记)和sweep(清扫)两个阶段. 朴素的Mark Sweep流程如下: @@ -192,7 +192,7 @@ freelist改成多条, 同一个大小范围的对象, 放在一个freelist上, 并发Sweep和并发Mark, 大大降低stw时间. -#### 并发收集: +#### 1.5 并发收集 朴素的Mark Sweep算法会造成巨大的STW时间, 导致应用长时间不可用, 且与堆大小成正比, 可扩展性不好. Go的GC算法就是基于Mark Sweep, 不过是并发Mark和并发Sweep. @@ -204,7 +204,7 @@ freelist改成多条, 同一个大小范围的对象, 放在一个freelist上, 首先backgroup sweep是比较容易实现的, 因为mark后, 哪些对象是存活, 哪些是要被sweep是已知的, sweep的是不再引用的对象, sweep结束前, 这些对象不会再被分配到. 所以sweep容和mutator内存共存, 后面我们可以看到golang是先在1.3实现的sweep并发. 1.5才实现的mark并发. -#### 写屏障 +#### 1.6 写屏障 接上面, mark和mutator同时运行就比较麻烦, 因为mutator会改变已被scan的对象的引用关系. @@ -240,7 +240,7 @@ mutaotr a.obj1=c 这一步, 将c的指针写入到a.obj1之前, 会先执行一段判断代码, 如果c已经被扫描过, 就不再扫描, 如果c没有被扫描过, 就把c加入到待扫描的队列中. 这样就不会出现丢失存活对象的问题存在. -#### 三色标记法 +#### 1.7 三色标记法 三色标记法是传统Mark-Sweep的一个改进, 由Dijkstra(就是提出最短路径算法的)在1978年发表的论文On-the-Fly Garbage Collection: An Exercise in Cooperation中提出. @@ -271,7 +271,7 @@ mutaotr a.obj1=c ### 2.Golang GC发展历史 -#### Golang GC简介 +#### 2.1 Golang GC简介 Golang对于GC的目标是低延迟, 软实时GC, 很多围绕着这两点来设计. @@ -295,7 +295,7 @@ Golang刚发布时(13,14年)GC饱受诟病, 相对于当时Java成熟的CMS(2002 [https://www.zhihu.com/question/42353634](https://links.jianshu.com/go?to=https%3A%2F%2Fwww.zhihu.com%2Fquestion%2F42353634) R大的回答 -#### Golang各版本GC改进简介 +#### 2.2 Golang各版本GC改进简介 以下是简单介绍Golang GC的版本更新以及STW时间(以下STW时间仅供参考, 因为STW时间与机器性能, 堆大小, 对象数量, 应用分配偏好, 都有很大的关系) @@ -315,7 +315,7 @@ GC's STW pauses in Go1.12 beta are much shorter than Go1.11 [https://www.reddit.com/r/golang/comments/aetba6/gcs_stw_pauses_in_go112_beta_are_much_shorter/](https://links.jianshu.com/go?to=https%3A%2F%2Fwww.reddit.com%2Fr%2Fgolang%2Fcomments%2Faetba6%2Fgcs_stw_pauses_in_go112_beta_are_much_shorter%2F) -#### Golang GC官方数据 +#### 2.3 Golang GC官方数据 以下GC的STW时间从网络上文章及官方分享中获取, 数据供参考, 但不会出现数量级的问题: @@ -335,7 +335,7 @@ Go 1.3-1.5 以下数据来自前google和前twitter工程师Brain Hatfield, 每隔半年在twitter上发表的一个服务升级Golang版本带来的GC提升. 这些数据在Golang GC掌门人Richard L. Hudson的分享上也列举过. 大概10GB级别堆从1.4-1.8的STW数据对比: [https://twitter.com/brianhatfield/status/804355831080751104](https://links.jianshu.com/go?to=https%3A%2F%2Ftwitter.com%2Fbrianhatfield%2Fstatus%2F804355831080751104) -#### Go 1.4-Go 1.5 +#### 2.4 Go 1.4-Go 1.5 2015/8月重新编译发布 @@ -347,7 +347,7 @@ Go 1.3-1.5 -#### Go1.5-Go 1.6 +#### 2.5 Go1.5-Go 1.6 2016年1月编译发布 @@ -359,7 +359,7 @@ Go 1.3-1.5 -#### Go 1.6-Go 1.7 +#### 2.6 Go 1.6-Go 1.7 2016年8月编译发布, Go1.6升级1.7后, 服务stw时间由3ms降为1-2ms @@ -369,7 +369,7 @@ Go 1.3-1.5 -#### Go 1.7-Go 1.8 +#### 2.7 Go 1.7-Go 1.8 2016年12月编译发布, 升级后, 服务STW时间由2-3ms->1ms以下 @@ -381,7 +381,7 @@ Go 1.3-1.5 -#### Go 1.9 +#### 2.8 Go 1.9 2017年8月 @@ -389,7 +389,7 @@ go 1.9在18GB堆下, stw时间, 都在1.0ms以下. go1.8版本之后, stw时间 -#### 总结图 +#### 2.9 总结图 Golang 1.4-1.9 STW时间与版本关系. @@ -405,11 +405,11 @@ Golang 1.4-1.9 STW时间与版本关系. Golang 1.8后GC的STW时间基本上做到了和堆大小无关. 而并发Mark时间则与存活对象数目(当然这个描述并不是非常准确)基本成正比, 与CPU的核数基本成反比. -#### 存活对象较少的堆 +#### 3.1 存活对象较少的堆 对于我们的业务系统来说, 一般都是有大量的临时对象. 反而总的存活对象不会很多. 我们先来看看堆里面存活对象不多的服务的GC情况. -##### 测试方式: +##### 测试方式 以下来自某服务. @@ -425,7 +425,7 @@ client调用(合并rpc和kv) 43W/min,7000qps/s (服务的qps倒不是很高, 主要是这个服务每次都会发6个rpc请求, 且没有优化过, 火焰图里可以看出来日志里序列化pb, 耗费了30%的性能) -##### 结果与分析: +##### 结果与分析 @@ -437,13 +437,13 @@ client调用(合并rpc和kv) 43W/min,7000qps/s 可以看出STW时间基本都在1ms以下, 有小部分超过2ms, 也比较正常. -#### 存活对象较大的堆 +#### 3.2 存活对象较大的堆 前面分析的是存活对象不多的情况. Mark Sweep是根据root来找到所有存活对象, 虽然堆很大, 但存活的对象不多, 所以mark时间也不会很大. 如果存活对象很多, 比如像缓存服务, golang的gc是怎样的情况呢? -##### 测试方式: +##### 测试方式 这里我写了一个代码来模拟. @@ -461,7 +461,7 @@ Mark Sweep是根据root来找到所有存活对象, 虽然堆很大, 但存活 -##### 分析: +##### 分析 由上可以看出, Golang GC Mark消耗的CPU时间与存活的对象数基本成正比(更准确的说是和需扫描的字节数, 每个对象第一个字节到对象的最后一个指针字段). 对于G级别以上的存活对象, 扫描一次需要花秒以上的CPU时间. @@ -473,11 +473,11 @@ Golang GC在存活对象非常多的情况下, 对CPU吞吐量的降低还是比 ### 4.Golang GC的一些演进规划 -标题有点标题党, 我们就不谈未来了, 我们谈谈Golang GC的一些规划和尝试. 这个部分主要参考Go的GC掌门人Richard L. Hudson在去年做的一个演讲. +谈谈Golang GC的一些规划和尝试. 这个部分主要参考Go的GC掌门人Richard L. Hudson在去年做的一个演讲. Getting to Go: The Journey of Go's Garbage Collector [Getting to Go: The Journey of Go's Garbage Collector](https://links.jianshu.com/go?to=https%3A%2F%2Fblog.golang.org%2Fismmkeynote) -#### Request Oriented Collector(面向请求的回收器) +#### 4.1 Request Oriented Collector(面向请求的回收器) 在2016年有一个propose是, Request Oriented Collector(面向请求的GC), 简单的说就是私有对象随着请求的结束而消亡. @@ -489,13 +489,13 @@ Go目前很大一个应用场景(Cloud Native?)是接受一个请求, 开一个 [https://blog.golang.org/ismmkeynote](https://links.jianshu.com/go?to=https%3A%2F%2Fblog.golang.org%2Fismmkeynote) -#### Generational GC(分代GC) +#### 4.2 Generational GC(分代GC) 对ROC尝试上的失败, Go团队转向分代GC.(从描述来看, 个人感觉ROC像是对分代GC的一种激进的做法. 主要思路也是为了减少对象的publish, 能回收掉的对象尽快回收掉.) 分代GC理论是80年代提出来的, 基于大部分对象都会在短时间内成为垃圾这个软件工程上的事实, 将内存分为多个区, 不同的区采取不同的GC算法. 分代GC并不是一种GC算法, 而是一种策略. -#### Java的分代GC +#### 4.3 Java的分代GC Java GC是分代GC的忠实拥簇者, 从98年发布的Java 1.2开始, 就已经是分代GC了. 图中是Java多种分代算法的默认分配情况. @@ -513,7 +513,7 @@ Java GC是分代GC的忠实拥簇者, 从98年发布的Java 1.2开始, 就已经 大部分时候只需要对新生代进行Minor GC,因为新生代空间小,Minor GC的暂停很短(生产服务中, 4核机器中500M-1G的新生代, GC大概为3ms-10ms的级别). 且绝大部分对象分配后就很快被Minor GC回收, 不会提升到老年代中, 对老年代Major GC的次数就会很少, 大大减少了频繁进行Major GC而Scan和Mark消耗的CPU时间, 减少回收大堆而导致的大的STW时间频次. 像API/RPC后台服务, 比较稳定的话, 可能几小时或一天才进行一次Major GC. -#### Golang的分代GC +#### 4.4 Golang的分代GC ##### 为何Golang对分代GC需求相对不大? diff --git "a/markdown/2020/20200109-golang\344\271\213gc\346\267\261\345\205\245\347\257\207.md" "b/markdown/2020/20200109-golang\344\271\213gc\346\267\261\345\205\245\347\257\207.md" new file mode 100644 index 0000000..39b471d --- /dev/null +++ "b/markdown/2020/20200109-golang\344\271\213gc\346\267\261\345\205\245\347\257\207.md" @@ -0,0 +1,394 @@ +[TOC] + +## 关于Golang GC的深入认识 + +Golang的GC是类似于Java的CMS GC,官方的mgc的注释如下: + +```text +// The GC runs concurrently with mutator threads, is type accurate (aka precise), allows multiple +// GC thread to run in parallel. It is a concurrent mark and sweep that uses a write barrier. It is +// non-generational and non-compacting. Allocation is done using size segregated per P allocation +// areas to minimize fragmentation while eliminating locks in the common case. +``` + +其中mutator是指我们的应用程序,因为可能会改变内存的状态,所以命名为mutator。这段话翻译过来,大概的意思就是说Go的GC使用的是一种非分代的没有整理过程的Concurrent Mark and Sweep算法(CMS算法),标记过程(即Mark过程)是使用三色标记法。再借用R大的一句话给问题先做个结论,“只要不移动对象做并发GC,最终就会得到某种形式的CMS。” + +### 标记-清理算法 + +标记-清理算法是一种追踪式的垃圾回收算法,并不会在对象死亡后立即将其清理掉,而是在一定条件下触发,统一校验系统中的存活对象,进行回收工作。 + +标记-清理分为两个部分,标记和清理,标记过程会遍历所有对象,查找出死亡对象。通过GC ROOT到对象的可达性就可以确认对象的存活,也就是说,如果存在一条从GC ROOT出发的引用最终可指向某个对象,就认为这个对象是存活的。这样,未能证明存活的对象就可以标记为死亡了。标记结束后,再次进行遍历,清理掉确认死亡的对象。 + +标记清理都是并发执行的标记-清理算法就是CMS。三色标记法是一种标记对象使用的算法。 + +### Go GC的改进历史 + +- 1.3以前的版本使用标记-清理的方式,整个过程都需要STW。 +- 1.3版本分离了标记和清理的操作,标记过程STW,清理过程并发执行。 +- 1.5版本在标记过程中使用三色标记法。回收过程主要有四个阶段,其中,标记和清理都并发执行的,但标记阶段的前后需要STW一定时间来做GC的准备工作和栈的re-scan。 +- 1.8版本在引入混合屏障rescan来降低mark termination的时间 + +#### 1. GC 1.5 + +
+ +
+ +1. Sweep Termination: 收集根对象,清理上一轮未清扫完的span,启用写屏障和辅助GC,辅助GC将一定量的标记和清扫工作交给用户goroutine来执行,写屏障在后面会详细说明。 +2. Mark: 扫描所有根对象和通过根对象可达的对象,并标记它们 +3. Mark Termination: 完成标记工作,重新扫描部分根对象(要求STW),关闭写屏障和辅助GC +4. Sweep: 按标记结果清理对象 + +#### 2. GC 1.8 + +1.8引入混合屏障,最小化第一次STW,混合屏障是指: +写入屏障,在写入指针f时将C对象标记为灰色。Go1.5版本使用的Dijkstra写屏障就是这个原理,伪代码如下: + +```text +writePointer(slot, ptr): + shade(ptr) + *slot = ptr +``` + +删除屏障,引入的Yuasa屏障伪代码如下: + +```text +writePointer(slot, ptr): + if (isGery(slot) || isWhite(slot)) + shade(*slot) + *slot = ptr +``` + +1.8中引入的混合屏障,写入屏障和删除屏障各有优缺点,Dijkstra写入写屏障在标记开始时无需STW,可直接开始,并发进行,但结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;Yuasa的删除写屏障则需要在GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象,但结束时无需STW。Go1.8版本引入的混合写屏障结合了Yuasa的删除写屏障和Dijkstra的写入写屏障,结合了两者的优点,伪代码如下: + +```text +writePointer(slot, ptr): + shade(*slot) + if current stack is grey: + shade(ptr) + *slot = ptr +``` + +因此,个人的理解是在Mark init阶段开始的时候激活混合写屏障这时候STW,在rescan阶段应该也只需要在去掉混合写屏障的时候STW。从算法上来看,是接近Java CMS算法,而非ZGC,当然Go GC的比Java CMS GC有很多实现上的优化。 + +#### 3. GC 1.12 + +在Go 1.12版本里,Go垃圾收集器依然使用非分代的并发的三色标记清理算法。Go的垃圾收集器的实现随着Go版本的变化而发生变化。因此,一旦发布下一版本,很多细节可能会有不同。 + +##### 3.1 垃圾收集器行为 + +Go垃圾收集器的行为分为两个大阶段Mark(标记)阶段和Sweep(清理)阶段。Mark阶段又分为三个步骤,其中两个阶段会有STW(Stop The World),另一个阶段也会有延迟,从而导致应用程序延迟并降低吞吐量,这三个步骤是: + +- Mark Setup 阶段- STW +- Marking阶段- 并发执行 +- Mark终止阶段 - STW + +下面一一讨论。 + +##### 3.2 Mark Setup阶段 + +垃圾收集开始时,必须执行的第一个动作是打开写屏障(Write Barrier)。写屏障的目的是允许垃圾收集器在垃圾收集期间维护堆上的数据完整性,因为垃圾收集器和应用程序将并发执行。 + +为了打开写屏障,必须停止每个goroutine。此动作通常非常快,平均在10到30微秒之内完成。 + +
+ +
+ +上图展示了在垃圾收集开始之前有四个goroutine在运行应用程序。为了暂停所有的goroutine,唯一的方法是让垃圾收集器观察并等待每个goroutine进行函数调用。等待函数调用是为了保证goroutine停止时处于安全点。如果其中一个goroutine不进行函数调用而其他goroutine执行函数调用,这种情况下会发生什么? + +
+ +
+ +上图展示了一个问题。在P4上运行的goroutine停止之前,垃圾收集无法启动。然而由于P4处于如下循环中,垃圾收集器可能无法启动。 + +```text +func add(numbers []int) int { + var v int + for _, n := range numbers { + v += n + } + return v +} +``` + +上面的代码片段是P4上正在执行的代码。go routine的运行时间取决于slice的大小。这段代码可以阻止垃圾收集器启动。更糟糕的是,当垃圾收集器等待P4时,其他P也无法提供服务。所以goroutines在合理的时间范围内进行函数调用对于GC来说是至关重要的。 + +##### 3.3 Marking阶段 + +一旦写屏障打开,垃圾收集器就开始标记阶段。垃圾收集器所做的第一件事是占用25%CPU。垃圾收集器使用Goroutines进行垃圾收集工作,. 这意味着对于一个4线程的Go程序,一个P将专门用于垃圾收集工作。 + +
+ +
+ +上图中P1专门用于垃圾收集。现在垃圾收集器可以开始标记阶段。标记阶段需要标记在堆内存中仍然在使用中的值。首先检查所有现goroutine的堆栈,以找到堆内存的根指针。然后收集器必须从那些根指针遍历堆内存图,标记可以回收的内存(译者注:标记的算法就是所谓的三色标记算法)。当标记工作在P1上进行时,应用程序可以在P2,P3和P4上继续进行。这意味着垃圾收集器的影响已最小化到当前CPU的25%。 + +这是理想的情况,然而现实却远没有如此简单。如果在垃圾收集过程中,P1在堆内存达到极限之前无法完成标记工作(因为应用程序可能在大量分配内存),该怎么办?如果3个Goroutines中只有一个大量分配内存导致P1无法完成标记工作,在这种情况下,分配新内存的速度会变慢,特别是始作俑者的那个Go routine分配内存的时候。 + +如果垃圾收集器确定需要减慢内存分配,原本运行应用程序Goroutines会协助标记工作。应用程序Goroutine成为Mark Assist(协助标记)中的时间长度与它申请的堆内存成正比。Mark Assist有助于更快地完成垃圾收集。 + +
+ +
+ +上图显示了在P3上运行的应用程序Goroutine现在正在执行Mark Assist并进行收集工作。 + +垃圾收集器的一个设计目标是减少对Mark Assists的需求。如果任何本次垃圾回收最终需要大量的Mark Assist才能完成工作,则垃圾收集器会提前开始下一个垃圾收集周期。这样做可以减少下一次垃圾收集所需的Mark Assist。 + +##### 3.4 Mark终止 + +一旦并发标记阶段完成,下一个阶段就是标记终止。最终关闭写屏障,执行各种清理任务,并计算下一个垃圾回收周期的目标。一直处于循环中的goroutine也可能导致stw延长(类似mark setup的情况)。 + +
+ +
+ +上图显示了在标记终止阶段完成时如何停止所有goroutine。这一动作平均在60到90微秒之间完成。这个阶段可以在没有STW的情况下完成,但是使用STW的代码更简单。 + +一旦收集完成,应用程序Goroutines就可以再次使用所有P,应用程序将恢复到油门全开的状态。 + +
+ +
+ +上图显示了垃圾收集完成后,所有P现在都可以用于应用程序。 + +##### 3.5 并发清理 + +标记完成后,下一阶段执行并发清理。清理阶段用于回收标记阶段中标记出来的可回收的内存。当应用程序goroutine尝试在堆内存中分配新内存时,会触发该操作。清理导致的延迟和吞吐量降低被分散到每次内存分配时。 + +下面是我的机器上的一个trace示例,其中有12个硬件线程可用于执行goroutine。 + +
+ +
+ +上图显示了部分trace快照。在这次垃圾收集过程中,三个P(总共12个P)专用于GC。你可以看到Goroutine 2450,1978和2696在这段时间里正在进行Mark Assist的工作,而不是执行应用程序。在Mark的最后,只有一个P专用于GC并最终执行STW(标记终止)工作。 + +垃圾收集完成后,除了你看到Goroutines下面有很多玫瑰色的线条之外,应用程序几乎恢复全力运行。 + +
+ +
+ +上图中那些玫瑰色线条代表Goroutine执行清理工作而非执行应用程序。这也是Goroutine试图分配新内存的时刻。 + +
+ +
+ +上图图显示了Sweep过程中Goroutines stack trace的一部分。调用runtime.mallocgc用于分配新内存。最终调用runtime.(*mcache).nextFree 执行清理。一旦所有可以回收的内存都回收完毕,就不再对nextFree进行调用。 + +刚刚描述的行为仅在垃圾收集启动并运行时发生。而GC百分比配置选项对于何时进行垃圾收集起着重要作用。 + + + +##### 3.6 GC百分比 + +运行时中有GC Percentage的配置选项,默认情况下为100。此值表示在下一次垃圾收集必须启动之前可以分配多少新内存的比率。将GC百分比设置为100意味着,基于在垃圾收集完成后标记为活动的堆内存量,下次垃圾收集前,堆内存使用可以增加100%。 + +举个例子,假设一个集合在使用中有2MB的堆内存。 + +注意:使用Go时,本文中堆内存的图表不代表真实情况。Go中的堆内存会碎片化。这些图只是示意图。 + +
+ +
+ +上图显示了最后一次垃圾完成后正在使用中的堆内存是2MB。由于GC百分比设置为100%,因此下一次收集会在在增加2 MB堆内存时启动。 + +
+ +
+ +上图显示增加2MB堆内存。这时触发一次垃圾收集。可以为每次GC都生成GC trace,就可以查看到相关动作。 + + + +##### 3.7 GC Trace + +在运行任何Go应用程序时,可以通过使用环境变量GODEBUG和gctrace = 1选项生成GC trace。每次发生垃圾收集时,运行时都会将GC trace信息写入stderr。 + +```text +GODEBUG=gctrace=1 ./app + +gc 1405 @6.068s 11%: 0.058+1.2+0.083 ms clock, 0.70+2.5/1.5/0+0.99 ms cpu, 7->11->6 MB, 10 MB goal, 12 P + +gc 1406 @6.070s 11%: 0.051+1.8+0.076 ms clock, 0.61+2.0/2.5/0+0.91 ms cpu, 8->11->6 MB, 13 MB goal, 12 P + +gc 1407 @6.073s 11%: 0.052+1.8+0.20 ms clock, 0.62+1.5/2.2/0+2.4 ms cpu, 8->14->8 MB, 13 MB goal, 12 P +``` + +上面展示了如何使用GODEBUG变量生成GC trace。同时显示了正在运行的Go应用程序生成的3条trace信息。 + +下面对GC trace中的每个值的含义进行的分解。 + +```text +gc 1405 @6.068s 11%: 0.058+1.2+0.083 ms clock, 0.70+2.5/1.5/0+0.99 ms cpu, 7->11->6 MB, 10 MB goal, 12 P + +// General +gc 1404 : The 1404 GC run since the program started +@6.068s : Six seconds since the program started +11% : Eleven percent of the available CPU so far has been spent in GC + +// Wall-Clock +0.058ms : STW : Mark Start - Write Barrier on +1.2ms : Concurrent : Marking +0.083ms : STW : Mark Termination - Write Barrier off and clean up + +// CPU Time +0.70ms : STW : Mark Start +2.5ms : Concurrent : Mark - Assist Time (GC performed in line with allocation) +1.5ms : Concurrent : Mark - Background GC time +0ms : Concurrent : Mark - Idle GC time +0.99ms : STW : Mark Term + +// Memory +7MB : Heap memory in-use before the Marking started +11MB : Heap memory in-use after the Marking finished +6MB : Heap memory marked as live after the Marking finished +10MB : Collection goal for heap memory in-use after Marking finished + +// Threads +12P : Number of logical processors or threads used to run Goroutines +``` + +上面显示了GC trace(1405)。最终将涉及其中大部分内容,但是现在只关注1045 GC trace的内存部分。 + +
+ +
+ +```text +// Memory +7MB : Heap memory in-use before the Marking started +11MB : Heap memory in-use after the Marking finished +6MB : Heap memory marked as live after the Marking finished +10MB : Collection goal for heap memory in-use after Marking finished +``` + +通过此GC trace可以看出,在标记工作开始之前,使用中的堆内存量为7MB。标记工作完成后,使用中的堆内存量达到11MB。这意味着在收集过程中有4MB新分配内存。标记工作完成后活动堆内存量为6MB。这意味着在下一次垃圾收集启动前,应用程序可以将堆内存增加到12MB。 + +你可以看到垃圾收集器Mark的目标和实际值之间有1MB差异。标记工作完成后正在使用的堆内存量为11MB而不是10MB。因为Mark目标是根据当前正在使用的堆内存量等信息计算出来的。应用程序的改变导致在Marking之后使用更多堆内存。 + +如果查看下一个GC trace(1406),可以看到在2ms内发生了很多变化。 + +
+ +
+ +```text +gc 1406 @6.070s 11%: 0.051+1.8+0.076 ms clock, 0.61+2.0/2.5/0+0.91 ms cpu, 8->11->6 MB, 13 MB goal, 12 P + +// Memory +8MB : Heap memory in-use before the Marking started +11MB : Heap memory in-use after the Marking finished +6MB : Heap memory marked as live after the Marking finished +13MB : Collection goal for heap memory in-use after Marking finished +``` + +这里显示了本次垃圾收集在上一次垃圾收集开始后2ms(6.068s对6.070s)就开始,使用中的堆内存达到8MB。由于应用程序大量分配内存,并且垃圾收集器希望减少此收集期间的因为Mark Assist导致的延迟,垃圾收集可能会提前。 + +还有两点需要注意。这次垃圾收集器完成了目标。标记完成后正在使用的堆内存量为11MB而不是13MB,少了2 MB。标记完成后活动堆内存依然是6MB。 + +可以通过添加gcpacertrace = 1从GC trace中获取更多详细信息。这会导致垃圾收集器打印有关并发起搏器内部状态的信息。 + +```text +$ export GODEBUG=gctrace=1,gcpacertrace=1 ./app + +Sample output: +gc 5 @0.071s 0%: 0.018+0.46+0.071 ms clock, 0.14+0/0.38/0.14+0.56 ms cpu, 29->29->29 MB, 30 MB goal, 8 P + +pacer: sweep done at heap size 29MB; allocated 0MB of spans; swept 3752 pages at +6.183550e-004 pages/byte + +pacer: assist ratio=+1.232155e+000 (scan 1 MB in 70->71 MB) workers=2+0 + +pacer: H_m_prev=30488736 h_t=+2.334071e-001 H_T=37605024 h_a=+1.409842e+000 H_a=73473040 h_g=+1.000000e+000 H_g=60977472 u_a=+2.500000e-001 u_g=+2.500000e-001 W_a=308200 goalΔ=+7.665929e-001 actualΔ=+1.176435e+000 u_a/u_g=+1.000000e+000 +``` + +运行GC trace可以告诉你很多关于应用程序的运行状况和收集器速度的信息。收集器运行的速度在收集过程中起着重要作用。 + +##### 3.8 起博 + +垃圾收集器使用调步算法,该算法用于确定何时开始垃圾收集。该算法依赖于运行中的应用程序的信息以及应用程序分配内存的压力。压力即应用程序在给定时间内分配堆内存的速度。正是压力决定了垃圾回收器的速度。 + +在垃圾收集器开始收集之前,它会计算预期完成垃圾收集的时间。一旦垃圾收集器开始运行,会影响正在运行的应用程序,造成延迟,拖慢用程序。每次收集都会增加应用程序的整体延迟。降低收集器的启动频率并非提高性能的方法。 + +可以将GC百分比值更改为大于100的值。这将增加在下一次收集启动之前可以分配的堆内存量。也导致垃圾收集时间更长。 + +
+ +
+ +上图显示了更改GC百分比如何允许分配的堆内存。 可以直观地了解使用更多对内存如何降低垃圾收集的速度。 + +降低收集器的启动频率无法帮助垃圾收集器更快完成收集工作。降低频率会导致垃圾收集器在收集期间完成更多的工作(译者注:因为分配了更多的内存)。 可以通过减少新分配对象数量来帮助垃圾收集器更快完成收集工作。 + +注意:这个做法同时可以用尽可能小的堆来实现所需的吞吐量。 在云环境中运行时,最小化堆内存等资源的使用非常重要。 + +
+ +
+ +上图显示了关于正在运行的应用程序的一些统计信息。蓝色版本显示的是没有优化的应用程序的统计信息。绿色版本是在去掉4.48GB的非生产性内存分配后的统计数据。 + +查看两个版本的平均收集速度(2.08ms vs 1.96ms),都约为2.0毫秒。这两个版本之间的根本变化在于每次垃圾收集时候的吞吐量。从3.98提高到了7.13个请求。吞吐量增加79.1%。垃圾收集的时间并没有随着内存分配的减少而减慢,而是保持不变。性能提升来自于每次垃圾收集期间,其他go routine可以完成更多工作。 + +调整垃圾收集的起博速度以推迟延迟成本并非提高应用程序性能的方式。 + +##### 3.9 收集器延迟成本 + +每次垃圾收集会造成两种类型的延迟。 首先是窃取CPU容量。 这种被盗CPU容量的影响意味着应用程序在垃圾收集过程中没有全速运行。应用程序Goroutines现在与垃圾收集器的Goroutines共享P或完成Mark Assist。 + +
+ +
+ +上图显示了应用程序使用75%的CPU工作。 这是因为收集器本身就有专用的P1。 + +
+ +
+ +上图显示了应用程序(通常只有几微秒)只能将其CPU容量的一半用于应用程序工作。 因为P3上的goroutine正在执行Mark Assist,而且垃圾收集器已经将P1占为己有。 + +第二种延迟是收集期间发生的STW延迟。 STW期间没有应用程序Goroutines执行任何应用程序。 该应用程序基本上已停止。 + +
+ +
+ +上图显示了所有Goroutines都停止的STW延迟。 每次垃圾收集都会发生两次。 如果应用程序正常运行,则垃圾收集器能够将大部分垃圾收集的总STW时间保持在100微秒或以下。 + +现在了解了垃圾收集器的不同阶段,内存的大小,调整的工作方式以及垃圾收集器对正在运行的应用程序造成的不同延迟。 有了这些知识,最终可以回答如何调优的问题。 + +##### 3.10 调优 + +减少堆内存的压力是最好的优化方式。 压力可以定义为应用程序在给定时间内分配堆内存的速度。 当堆内存压力减小时,垃圾收集器造成的影响会减少。减少GC延迟的方法是从应用程序中识别并去掉不必要的内存分配。 + +以下做法可以帮助垃圾收集器: + +- 尽可能保持最小的堆。 +- 最佳的一致的起博频率。 +- 保持在每次收集的目标之内。 +- 最小化每次垃圾收集的STW和Mark Assist的持续时间。 + +所有这些都有助于减少垃圾回收造成延迟,也将提高应用程序的性能和吞吐量。 垃圾收集的频率与此无关。 + +了解工作量意味着确保使用合理数量的goroutine来完成工作。 CPU瓶颈与IO瓶颈的工作负载不同,需要不同的工程决策,可以参考本文。[https://www.ardanlabs.com/blog/2018/12/scheduling-in-go-part3.html](https://link.zhihu.com/?target=https%3A//www.ardanlabs.com/blog/2018/12/scheduling-in-go-part3.html) + +了解数据意味着了解虚要解决的问题。 数据语义一致性是维护数据完整性的关键部分,并允允许你决定在堆上还是栈上分配内存。[https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html](https://link.zhihu.com/?target=https%3A//www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html) + +##### 3.11 结论 + +对Go语言运行时来说重要的是要认识到有效的内存分配(帮助应用程序的分配)和那些没有无效的内存分配(那些损害应用程序)之间的差异。 然后就只能信任垃圾收集器可以高效的运行。 + +拥有垃圾收集器是一个很好的权衡。 虽然有垃圾收集的成本,但是却没有内存管理的负担。 Go语言同时兼顾了开发和运行效率。 垃圾收集器是实现这一目标的重要组成部分。 + +**参考** + +- https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html + +- https://zhuanlan.zhihu.com/p/77943973 \ No newline at end of file diff --git "a/markdown/2020/20200119api-gateway\346\234\215\345\212\241panic\351\227\256\351\242\230\346\225\264\347\220\206.md" "b/markdown/2020/20200119api-gateway\346\234\215\345\212\241panic\351\227\256\351\242\230\346\225\264\347\220\206.md" new file mode 100644 index 0000000..9663c26 --- /dev/null +++ "b/markdown/2020/20200119api-gateway\346\234\215\345\212\241panic\351\227\256\351\242\230\346\225\264\347\220\206.md" @@ -0,0 +1,260 @@ +[TOC] + +#### api-gateway服务panic问题整理 + +##### 1.问题现象(2020-01-11) + +1月11日下午,线上网关服务api-gateway(golangServer)偶现报`panic`异常,日志如下: + +```go +2020/01/11 14:44:23 [ERROR] [Recovery]#panic# recovered:POST /user/getUserDeviceList HTTP/1.0 +Host: xxx.xxx.xxx.net +Connection: close +Accept-Encoding: gzip +Connection: close +Content-Length: 185 +Content-Type: application/x-www-form-urlencoded +User-Agent: beegoServer +X-Forwarded-For: 10.xx.xx.140 +X-Real-Ip: 10.xx.xx.140 + + +write tcp 127.0.0.1:8878->127.0.0.1:52942: write: broken pipe +goroutine 1930665817 [running]: +runtime/debug.Stack(0x0, 0xc4258b43c0, 0x11a) + /home/go/src/runtime/debug/stack.go:24 +0xa7 +server/httpserver/middleware.IntercepteHnadler.func1.1(0xc420dc46e0, 0xc420bbe720, 0x24) + /home/api-gateway/src/server/httpserver/middleware/middlerwares.go:41 +0xbb +panic(0xb07500, 0xc42588d310) + /home/go/src/runtime/panic.go:502 +0x229 +vendor/github.com/gin-gonic/gin/render.JSON.Render(0xaef060, 0xc4261ef530, 0x7f00101fb2a8, 0xc425b7ac20, 0xc420dc4600, 0x7f00101fb2a8) + ... +``` + +报错为:`broken pipe`,这个问题归类为客户端tcp连接被异常kill掉,导致服务端对socket写入时收到了RST响应,二次写入出现了`broken pipe`现象. + +##### 2.问题分析 + +再次查看nginx日志,发现日志并未正常导出。 + +分析当前业务的请求链路: + +*三方业务* --> *nginx* --> *api-gateway(golang server)* --> *backend* + +(1).怀疑nginx子进程由于异常被master进程kill: + +​ 但是深究并不合理,一个子进程被kill,会导致该进程的上百个请求报错,同时从linux查看nginx的进程启动的时间,发现近期并没有**重启**的子进程 + +(2).怀疑client请求连接数量,超过nginx配置**worker_connections**值,导致nginx内部tcp连接被nginx进程close.通过监控查看机器当时的qps仅800左右,nginx作为反向代理服务器,需要占用1600个fd描述符: + +而nginx的worker_connections配置为1024,共16个nginx子进程,根据linux命令: + +```shell +ps -ef | grep nginx | grep -v grep | grep -v master | awk '{print $2}' | xargs -i ls -l /proc/{}/fd | grep -E "total|socket" | awk 'BEGIN{i=0}{i++;if($1=="total"){print "fd_count:"i;i=0}}END{print "fd_count:"i}' +``` + +​ 查看16个子进程的fd数量分布: + +```php +fd_count:24 +fd_count:28 +fd_count:43 +fd_count:55 +fd_count:78 +fd_count:89 +fd_count:99 +fd_count:108 +fd_count:123 +fd_count:143 +fd_count:160 +fd_count:168 +fd_count:197 +fd_count:202 +fd_count:254 #254个socket连接 +fd_count:69 +``` + + +​ 根据相关资料: nginx子进程最大client连接为1024/2= 512 左右,仍然可能并不是问题原因。这里做优化点,调整worker_connections配置 + +##### 3.解决方案(2020-01-14) + +- 1.增加nginx error_log +- 2.增加api-gateway的panic的request_id +- 3.修改nginx worker_connections连接配置,修改为4096 +- 4.修改nginx buffer缓冲区 + +以上方案于1月14号上线,并继续观察。 + +##### 4.线上观察(2020-01-16) + +通过之前增加的日志,发现nginx报如下error日志: + +``` +//nginx机器ip +[yushaolong@p34957v nginx]$ cat error.log +2020/01/16 06:39:07 [warn] 13876#0: 4096 worker_connections exceed open file resource limit: 1024 + +//nginx机器ip +2020/01/16 04:11:37 [warn] 22449#0: *1022232468 a client request body is buffered to a temporary file /data/nginx/client_body_temp/0000002378, client: xx.77.xx.146, server: xx.xx.xx.cn, request: "POST /wls-wsat/CoordinatorPortType HTTP/1.1", host: "xx.xx.59.100" +2020/01/16 04:11:38 [error] 22449#0: *1022232468 writev() failed (104: Connection reset by peer) while sending request to upstream, client: xx.77.xx.146, server: xx.xx.xx.cn, request: "POST /wls-wsat/CoordinatorPortType HTTP/1.1", upstream: "http://127.0.0.1:8878/wls-wsat/CoordinatorPortType", host: "xx.xx.59.100" +``` + +通过日志发现`104: Connection reset by peer`是由于buffer太小导致,参考相关资料, 而buffer太小可能是导致golangServer写入nginx时panic的原因。此次需要继续调优`nginx` + +- 1.修改worker_rlimit_nofile 10240 +- 2.增大buffer缓冲区 + +继续上线观察。 + +##### 5.线上观察(2020-01-17) + +1月17日上午api-gateway的panic问题再次重现。由于前面做了准备,这次直接登上panic服务查看日志: + +``` +//api-gateway机器ip +2020/01/17 10:45:07 [ERROR] [Recovery]#panic# requestId:8e92d5a2-bac7-4d88-a05a-b1ac6a843b60,recovered:POST /user/getUserDeviceList HTTP/1.0^M +Host: xx.xx.xx.net^M +Connection: close^M +Accept-Encoding: gzip^M +Connection: close^M +Content-Length: 185^M +Content-Type: application/x-www-form-urlencoded^M +User-Agent: beegoServer^M +X-Forwarded-For: 10.209.158.198^M +X-Real-Ip: 10.209.158.198^M +^M + +write tcp 127.0.0.1:8878->127.0.0.1:22705: write: broken pipe +``` + +根据日志里的request_id查询api-gateway及backend响应的日志链路: + +``` +//api-gateway.log +2020/01/17 10:45:07 [INFO] [backend-proxy] requestId:8e92d5a2-bac7-4d88-a05a-b1ac6a843b60, endpointPath:/user/getUserDeviceList,backendHost:http://xx.xx.xx.183:80,backendPath:/user/getUserDeviceList,timeUsed(ms):3412,err: + +//backend.log +2020/01/17 10:45:07 [INFO] __Reqeust_Result__||protocol=http||requestId=8e92d5a2-bac7-4d88-a05a-b1ac6a843b60,statusCode=200,clientIP:xx.xx.xx.1 +98,method:POST,path:/user/getUserDeviceList,userAgent:beegoServer,request: +.. +约157KB +.. +``` + +在api-gateway的日志中发现backend已经正常响应,耗时**3412ms**,说明整个请求链路中问题确定出在`nginx-->api-gateway`这个阶段。由于之前nginx的error日志无法正常输出的问题已经修复,此次期待查看nginx的相关error.log会有所收获。但是却意外的发现**nginx此时无错误日志**。这种情况便推翻了之前的怀疑方向,因为nginx并无报错。难道由于正常超时的原因? 整理了调用链路图及相关超时配置: + +
+ +
+ +分析发现:nginx的`keepalive_timeout=30s`, 而http_server连接池的维护时间`idle_timeout=28s`.而此次panic时请求backend耗时3s左右,是由于超过了nginx的timout导致nginx正常关闭? 修改nginx的keepalive时间: + +- 修改`keepalive_timeout`为60s + +继续上线观察。 + +##### 6.重要线索 + +1月17日下午, @张磊同学在追踪上午的问题时, 在nginx的access.log中发现了如下日志: + +``` +10.xx.158.xx - - [17/Jan/2020:10:45:07 +0800] "POST /user/getUserDeviceList HTTP/1.1" 499 0 "-" "beegoServer" "-" +``` + +这条日志显示nginx响应状态码为`499`,表示client在请求nginx时,由于client设置了超时时间,而nginx并未在超时时间内响应,导致client主动关闭连接。据相关资料[nginx 499](https://forum.nginx.org/read.php?2,253026,253026),nginx收到499之后,会对api-gateway的连接进行关闭。难道是这个原因?细思又不合理,因为又发现nginx一天中会产生很多诸如499的状态码: + +``` +... +10.xx.171.xx - - [17/Jan/2020:00:12:02 +0800] "POST /device/updateEvent HTTP/1.1" 499 0 "-" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1)" "-" +10.xx.139.xx - - [17/Jan/2020:00:15:02 +0800] "POST /device/updateEvent HTTP/1.1" 499 0 "-" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1)" "-" +10.xx.xx.3 - - [17/Jan/2020:00:15:02 +0800] "POST /device/updateEvent HTTP/1.1" 499 0 "-" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1)" "-" +10.xx.xx.198 - - [17/Jan/2020:00:30:03 +0800] "POST /device/updateStatus HTTP/1.1" 499 0 "-" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1)" "-" +... +``` + +这种情况下,应该会有大量的panic出现,但是事实却是寥寥无几。然而在把所有的线索都串了起来之后,突然发现了些什么: + +``` +1.api-gateway最近的panic都来源于接口/user/getUserDeviceList +2.与client端同学沟通,他们的请求超时设为3s +3.nginx的keeptimeout为30s, 而golang的idle_timeout为28s +4.panic时,/user/getUserDeviceList请求时间都大于3s (推断) +5.panic时,nginx一定会出现499的状态码(推断) +``` + +如上,1,2,3为事实,4,5为推断。所以原因断言如下: + +**巧合,偶现的问题确实都是巧合,因为不同于必现。** + +(1) client单方面超时,nginx报499后,因为nginx存在连接池,499后并不会立即关闭socket fd. 而是查看该fd是否keeptimeout过期。 + +(2)nginx正巧此时keepalive_timeout超过30s,此次client的499导致nginx关闭该socket fd. + +(3)而api-gateway此时正在访问后端服务,当访问后端服务结束后,由于nginx已经关闭fd,所以api-gateway第一次写入nginx时收到tcp的RST响应,第二次写入导致panic. + +仅为断言,预计修改nginx的keepalive_timeout为60s,该问题不会出现。 继续观察~ + +##### 7.问题定位 + +*拨开迷雾见晴天* + +在问题没有被定位之前,断言是可以继续被质疑。于是内心又生出疑惑: + +- 为什么断言nginx的499不会立即关闭proxy的fd? + + ``` + 这个断言是错误的,nginx会在收到499后立即关闭nginx与apigateway的连接。 + 与keepalive_timeout无关,可以参见nginx源码及论坛[https://forum.nginx.org/read.php?2,253026,253026] + ``` + +- 为什么panic的日志里,http是1.0及connection:close? + + ``` + 这个问题,起初认为是client的同学在用http1.0协议,感觉很古董。但是日志里显示的用的是beego框架,beego的http协议默认是1.1。所以不能单纯的认为是client传来的。 + 深入了解后发现,这个http1.0其实是nginx请求api-gateway时带来的。[https://blog.csdn.net/wangkai_123456/article/details/71715852] + ``` + +- client与nginx, nginx与api-gateway之间真的都用到长连接? + + ``` + //client与nginx之间的连接 + 由于nginx配置了keepalive_timeout,所以nginx是支持长连接的。但是需要client使用时复用http对象,否则每次client与nginx交互都是短连。 + //nginx与api-gateway + nginx与api-gateway之间,目前的调用方式其实是短连,api-gateway与后端请求建连处理结束后,连接便被nginx关闭。如果需要长连,则配置upstream及proxy_http_version如下: + upstream manager_backend { + server 127.0.0.1:8095; + keepalive 16; + } + proxy_http_version 1.1; + proxy_set_header Connection ""; + ``` + +解决了以上几个疑惑后,发现断言是不合理的。但是nginx 499状态码在请求中大量出现,为什么api-gateway的panic却是偶现? + +逐渐接近了真相: [golang buffer 8kb](https://stackoverflow.com/questions/43189375/why-is-golang-http-server-failing-with-broken-pipe-when-response-exceeds-8kb),产生panic的原因描述如下: + +首先client由于自身业务超时单方面断开连接,而nginx收到client断开连接的请求时,会立即以异常(reset)的方式关闭与api-gateway之前建立的socket连接,并打印499日志。 + +在大部分情况下,api-gateway服务这时还在等待backend的数据响应。等backend响应数据后,api-gateway会将响应数据按照批次,分批写入golang内置的buffer缓冲区,当缓冲区写满时,会一次性将数据通过socket连接刷到nginx。 + +第一次通过socket连接将buffer发送给nginx时,nginx作为客户端会向api-gateway的tcp层响应RST报文,所以api-gateway第一次向nginx发送数据是不报错的,但api-gateway的tcp层会设置RST的标记。当api-gateway作为应用层第二次通过底层tcp向nginx发送数据时,socket则会报broken pipe错误。 + +golang net.http 包内置的buffer大小为4kb,8kb.而业务中大部分499的状态,其实只有100byte左右的响应。但panic时,会发现backend响应的数据报文超过100kb。 + +上述修改keepalive_timeout为60s并没有实际解决问题,解决方案: + +- proxy_ignore_client_abort设为on + +##### 8.结论 + +以上便是记录分析本次问题时的思考过程。问题出现之后,其实解决的方向很重要。当然解决问题需要不断的探索和尝试,这也是自我学习及提高的过程。上述若有不足之处欢迎指正。 + + + +##### 参考 + +- *nginx workerconnection:* https://www.imooc.com/article/19907 +- *nginx 499:* https://forum.nginx.org/read.php?2,253026,253026 +- *nginx proxy:* https://blog.csdn.net/wangkai_123456/article/details/71715852 +- *golang buffer 8kb:* https://stackoverflow.com/questions/43189375/why-is-golang-http-server-failing-with-broken-pipe-when-response-exceeds-8kb diff --git "a/markdown/2020/20200316-\347\272\277\344\270\212nginx\350\200\227\346\227\266\351\227\256\351\242\230\346\216\222\346\237\245.md" "b/markdown/2020/20200316-\347\272\277\344\270\212nginx\350\200\227\346\227\266\351\227\256\351\242\230\346\216\222\346\237\245.md" new file mode 100644 index 0000000..b37453f --- /dev/null +++ "b/markdown/2020/20200316-\347\272\277\344\270\212nginx\350\200\227\346\227\266\351\227\256\351\242\230\346\216\222\346\237\245.md" @@ -0,0 +1,246 @@ +[TOC] + +### 线上nginx耗时问题排查 + +#### 1.问题现象 + +最近客户端同学反馈, 服务端getDeviceUserList接口耗时超过了3s, 由于他们设置了3s超时报警,最近一直收到线上偶发性的报警。根据客户端同学反映的request_id(请求唯一标识), 我们发现服务端api-gateway的耗时日志: + +``` +2020/03/12 16:40:12 [INFO] +__Request__:5cfb9f36-71f1-4885-9a6f-9ae2faec624a +statusCode:200 clientIP:10.209.146.152 method:POST +path:/user/getDeviceUserList userAgent:beegoServer ... +response:{"data":[{"bind_ts":"1573464023","qid":1021551511,"roles":"owner"}],"errmsg":"ok","errno":0,"request_id":"5cfb9f36-71f1-4885-9a6f-9ae2faec624a"} +timeUsed:1ms +``` + +日志中显示服务端处理耗时是1ms, 但客户端却说是3s超时? + +目前线上服务单机qps约为800左右,机器上同时部署了nginx和api-gateway(基于golang开发)。请求链路如下: + +``` +client -> lvs -> (nginx->api-gateway) +``` + +client发起请求到nginx, 反向代理到api-gateway,而目前耗时1ms的日志是api-gateway输出。所以问题可能出现在client和nginx. + +#### 2.问题分析 + +客户端同学提出是不是由于服务器上nginx的backlog太小,导致客户端请求建立TCP连接时发送的sync包被服务端丢弃: + +``` +#查看linux上的nginx服务的accept队列 +> ss -pl +State Recv-Q Send-Q Local + ... +LISTEN 0 511 *:https +LISTEN 0 511 *:http + ... +``` + +显示`Send-Q`为511, `Recv-Q`为0,说明nginx的backlog长度为511(默认值),当前客户端请求连接无堆积。 + +> **当State为LISTEN状态: Recv-Q 表示当前等待服务端调用 accept 完成三次握手的 listen backlog 数值; Send-Q 表示最大的 listen backlog 数值** + +同时我们查询了linux上socket queue的overflowed次数: + +``` +#任何一个包含dropped或者overflowed并且数值一直居高不下的都不是好现象 +> netstat -s +TcpExt: + ... + 52 times the listen queue of a socket overflowed + 52 SYNs to LISTEN sockets ignored +``` + +并未发现异常,不过我们仍修改了nginx配置做了此处优化: + +``` +#nginx的server配置,修改backlog为65535 +listen 80 backlog=65535; +``` + +优化之后,客户端同学反馈问题依然存在,由于早期配置nginx时,并没有打印请求耗时,所以我们修改了nginx的配置,增加耗时相关: + +``` +#nginx的耗时 $request_time $upstream_response_time +http { + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" $request_time $upstream_response_time'; + ... +} +``` + +不过由于nginx是业界成熟代理的观念深入人心,主观认为问题应该不在nginx, 所以目光转向client, 是不是客户端自己的问题? + +我们与客户端同学沟通并review了其相关源码(基于go1.11并使用beego的http库),我们依据他们的编码方式,针对性的编写了相关代码对服务端接口进行了压测,但仍未得到结论。此时的关注点主要在客户端和网络上,并整理了如下个人疑惑的一些问题: + +``` +#疑惑问题 +1.client出现超时情况时调用服务端接口都是getDeviceUserList? 这个接口请求的正常耗时是多少? +2.client进行ping服务器的耗时是多少? +3.报警机器当前的网络状态是什么情况? netstat -anp | grep TIME | wc -l, 系统有没有进行内核调优 +4.报警机器网卡的流量监控? 带宽是多少? +5.客户端出现问题的机器是所有机器?还是某一台,某几台? +``` + +但客户端同学反映抓到了请求服务端超时的TCP包,就为问题出现在服务端敲定了实锤。 + +``` +#客户端请求服务端的tcp包 +#(1) 16:40.09,客户端向服务端发送http请求包 +16:40:09.692570 IP (tos 0x0, ttl 64, id 21792, offset 0, flags [DF], proto TCP (6), length 431) cron-iotevent-prod-bac4fe-cbdcc6589-v2h6w.49558 > 10.209.221.234.80: Flags [P.], cksum 0x87c6 (incorrect -> 0x5258), seq 1:392, ack 1, win 229, length 391: HTTP, length: 391 +#(2) 16:40.09,服务端ack收到窗口数据 +16:40:09.693638 IP (tos 0x0, ttl 56, id 31066, offset 0, flags [DF], proto TCP (6), length 40) 10.209.221.234.80 > cron-iotevent-prod-bac4fe-cbdcc6589-v2h6w.49558: Flags [.], cksum 0x2c8d (correct), ack 392, win 123, length 0 +#(3) 16:40.12,服务端向客户端响应http +16:40:12.706953 IP (tos 0x0, ttl 56, id 31067, offset 0, flags [DF], proto TCP (6), length 417) 10.209.221.234.80 > cron-iotevent-prod-bac4fe-cbdcc6589-v2h6w.49558: Flags [P.], cksum 0x0fa7 (correct), seq 1:378, ack 393, win 123, length 377: HTTP, length: 377 +``` + +#### 3.问题解决 + +再次检查nginx的请求日志,的确发现了超过3s的日志,但是很奇怪api-gateway日志中响应明明是1ms,为什么nginx的日志耗时却是3s? 重新梳理了问题现状: + +``` +#请求链路 +client-->nginx-->api-gateway +#现有依据 +1.客户端抓包发现 http请求与响应差3s +2.api-gateway日志显示请求耗时1ms, 并在请求的第3s打印 +3.nginx error日志出现批量3s的现象 +``` + +分析得出,当nginx向api-gateway发送http请求时消耗了超过2.99s,原因可能是: + +- nginx请求api-gateway的连接出现了拥堵 +- nginx内部某些原因的性能消耗 + +目前nginx配置请求下游时使用的是短连接, 所以将nginx配置修改为长连。同时对线上服务进行压测,使用strace分析nginx进程性能时,发现write部分(日志)占用了23%的耗时: + +``` +#strace分析进程执行命令,30897为nginx的一个worker进程id +> strace -c -p 30897 #压测30s数据 +% time seconds usecs/call calls errors syscall +------ ----------- ----------- --------- --------- ---------------- + 37.41 0.038220 7 5782 3417 accept4 + 33.51 0.034240 4 9077 epoll_wait + 23.26 0.023760 5 4730 write + 1.99 0.002031 0 4730 close + 1.45 0.001485 0 4730 writev + 1.04 0.001061 0 2365 2365 connect + 0.46 0.000465 0 4730 recvfrom + 0.29 0.000294 0 2365 socket + 0.25 0.000256 0 9078 gettimeofday + 0.25 0.000251 0 7095 epoll_ctl + 0.06 0.000061 0 2365 ioctl + 0.04 0.000045 0 2365 getsockopt +------ ----------- ----------- --------- --------- ---------------- +100.00 0.102169 59412 5782 total +``` + +将nginx打印日志配置buffer缓冲,并分别设置256k,512k,1024k进行压测调优,发现buffer配置为512k时,write耗时性能最优(耗时比占用0.5%),所以修改nginx配置如下: + +``` +#nginx配置 +#使用http长连 +upstream backend { + server 127.0.0.1:8878; + keepalive 16; #每个worker维持16个http长连 +} + +#增大nginx的log日志buffer为512k +server { + ... + access_log /data/log/nginx/access_proxy-80.log main buffer=512k; +} +``` + +修改上线之后,监控尚未发现耗时超过3s的日志,问题已经解决。 + +#### 4.新的问题 + +nginx配置修改上线1天之后,发现nginx极少的error日志: + +``` +2020/03/16 02:05:02 [error] 21533#0: *8125632065 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 10.209.156.213, server: api.iot.360.cn, request: "POST /user/getDeviceUserList HTTP/1.1", upstream: "http://127.0.0.1:8878/user/getDeviceUserList", host: "10.209.221.234" +``` + +> *果然是解决了一个问题的同时,又出现了另一个问题~* + +对于`connection reset by peer`问题,一顿百度google之后,没有发现令人满意的解决方案。又重新开始思考http1.1协议及keep alive,并发现了之前自己错误的观点: + +``` +#[错误]如下nginx观点错误 +一直以为nginx(1.14.2)里的upstream长连使用的超时时间是keepalive_timeout, +比如设定keepalive_timeout=25s,则表示一个upstream长连最多维持25s(生命周期), +包括基于这条连接的所有请求,处理,响应,空闲时间。 + +#[错误]如下golang的net.http包 httpServer观点错误 +server := http.Server{ + ... + IdleTimeout: time.Second * 28, //空闲连接超时时间 + } +一直以为idleTimeout指的是一个http长连维护最多28s(生命周期), 包括这期间的所有请求,处理,响应,空闲时间。 +``` + +其实http1.1的keepalive指的是连接空闲时的超时时间,不包括请求,处理,响应时间,如果一个http长连接一直有流量,并且两次流量的间隔时间不超过keepalive设定,则这条连接会一直存活。**nginx的upstream其实并没有超时时间,一旦建立,不会主动关闭,只有服务端主动发送FIN包,nginx才会把ack这个连接关闭。** + +那为什么nginx会出现connect reset by peer呢?使用tcpdump抓包之后,原因就水落石出: + +``` +#使用tcpdump抓包 +#80为nginx对外暴露http服务的端口 +#8878为api-gateway的http监听端口 +> tcpdump -i any port 8878 -nn +... +#nginx向api-gateway发送http请求(正常) +02:04:34.085819 IP localhost.56316 > localhost.8878: Flags [P.], seq 799687:800138, ack 492088, win 1024, options [nop,nop,TS val 2055170395 ecr 2055156376], length + 451 +#api-gateway响应窗口,响应http数据(正常) +02:04:34.087436 IP localhost.8878 > localhost.56316: Flags [P.], seq 492088:492419, ack 800138, win 1024, options [nop,nop,TS val 2055170396 ecr 2055170395], length + 331 +#nginx响应窗口(正常) +02:04:34.087446 IP localhost.56316 > localhost.8878: Flags [.], ack 492419, win 1022, options [nop,nop,TS val 2055170396 ecr 2055170396], length 0 +#间隔了28s15ms, nginx向api-gateway发送http请求 +02:05:02.102019 IP localhost.56316 > localhost.8878: Flags [P.], seq 800138:800589, ack 492419, win 1024, options [nop,nop,TS val 2055198411 ecr 2055170396], length + 451 +#因为api-gateway配置的http超时时间是28s,此时apigateway准备,但尚未向nginx发送FIN包 +#但是nginx也巧合的在刚刚超时的时间点先PUSH了一个包 +#所以api-gateway响应reset包 +02:05:02.102980 IP localhost.8878 > localhost.56316: Flags [R.], seq 492419, ack 800589, win 1024, options [nop,nop,TS val 2055198412 ecr 2055198411], length 0 + ... +``` + +由于在api-gateway准备对连接进行超时关闭时(idleTimeout为28s),nginx却巧合在此时仍然通过该连接发送请求,所以收到了R包,报了`reset by peer`.在工程实践上,这种问题只有靠双方不停的心跳去解决。但是nginx(1.14.2版本),尚无对upstream长连接进行超时的配置,所以暂无较好的解决方案。如下为两种尝试方案: + +``` +1.api-gateway的超时设置为最大,或者永不超时 +2.将nginx的upstream keepalive去掉,使用http短连 +``` + +鉴于此种情况出现率极低,在业务层面可以容忍,所以暂时忽略。 + +#### 5.问题结论 + +目前线上服务器8core16g,在单机qps 800左右时,偶现了nginx请求api-gateway超时情况,对nginx最终做了如下优化: + +``` +#1.增加了nginx的backlog +listen 80 backlog=65535; +#2.修改upstream为长连接 +upstream backend { + server 127.0.0.1:8878; + keepalive 16; #每个worker维持16个http长连 +} +#3.增加日志buffer +server { + ... + access_log /data/log/nginx/access_proxy-80.log main buffer=512k; +} +``` + +但同时导致了`connection reset by peer`问题,并分析了其原因。 + +*果然是填了一个坑,又挖了一个新坑,把这个坑留给以后的同胞们去填!* + diff --git "a/markdown/2020/20200318-mysql\347\237\245\350\257\206\346\225\264\347\220\206.md" "b/markdown/2020/20200318-mysql\347\237\245\350\257\206\346\225\264\347\220\206.md" new file mode 100644 index 0000000..e7e6e4f --- /dev/null +++ "b/markdown/2020/20200318-mysql\347\237\245\350\257\206\346\225\264\347\220\206.md" @@ -0,0 +1,166 @@ +[TOC] + +### mysql + +#### 1.分库分表 +``` +参考: +https://s3.uczzd.cn/webview/news?app=uc-iflow&aid=6782793340569264804&cid=100&zzd_from=uc-iflow&uc_param_str=dndsfrvesvntnwpfgicp&recoid=10326025693030819870&rd_type=share&sp_gz=0&pagetype=share&btifl=100&uc_share_depth=1 +``` + +##### 1.1 引出分布式事物 + +#### 2.行溢出 +- page页 16KB + + ``` + 参考: https://mp.weixin.qq.com/s?__biz=MzIxNTQ3NDMzMw==&mid=2247483670&idx=1&sn=751d84d0ce50d64934d636014abe2023&chksm=979688e4a0e101f2a51d1f06ec75e25c56f8936321ae43badc2fe9fc1257b4dc1c24223699de&scene=21#wechat_redirect + ``` + +#### 3.索引 + +##### 3.1索引结构 + +B+树, +根部索引 +页16kb/(8主键+6指针) = 1000左右,3级b+树 `1000*1000*(16kb/1kb)` = 1000w +二级索引走主索引 + +``` +参考: +https://www.toutiao.com/a6740133818021184004/?uc_share_depth=1 +``` + +##### 3.2 索引分页 + +使用offset,limit时,当offset越大,查询越慢的原因: +- 如果查询条件为`select * from`, 则查询越慢, +- 如果查询条件为`select id from`,查询并不慢 +``` +参考: +https://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ +``` + +##### 3.3 页分裂,页合并 + +mysql以页为单位进行操作。 + +(1)页合并 + +指对mysql数据进行delete及update操作时,发生多个页合成一个页的情况。如果删掉数据之后所占当前页大小, 或者更新数据后的页大小,与临近页在策略上可以融合填充。 + +(2)页分裂 + +当某一条新增数据,检测填充前一个页尾,空间不够;填充后一个页页头,空间也不够,这时mysql会重新分配一个新页,让前一个页指向新页,新页指向后一个页。新页可能不在相邻扇区,所以影响性能,除非新页被重新merge,或者使用OPTIMIZE TABLE. + +``` +知乎精选: https://zhuanlan.zhihu.com/p/98818611 +``` + +(3) 为啥推荐自增 id 作为主键? + +> 表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。这样就会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。 + +如果不使用自增id: + +- 对于data数据层,将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。 +- 对于index索引层,也会导致频繁的重新构建B+树索引,出现页分裂的情况 + +``` +https://mp.weixin.qq.com/s/svL_yNmNJ-wj9aGjtO_Yww +``` + + + +#### 4.Innodb引擎 + +##### 4.1 事物的原子性 + +由于要提高性能,需要并发事物,导致数据丢失,脏读,不可重复读, 幻读带来的问题 + +- 数据丢失由应用层面进行锁分离 + +- 脏读,不可重复读, 幻读 + +##### 4.2 引出事物隔离机制 + +- 事物之间的锁 (innodb在索引加行锁,主键索引、唯一索引或普通索引) + + ``` + 参考:https://www.cnblogs.com/aipiaoborensheng/p/5767459.html + 不同事物之间的并发,引入锁 + 在不同的事物隔离机制下,不同的sql使用s锁和x锁 + select update insert + 读未提交 nolock x x + 读已提交 nolock x x + 可重复读 nolock x x + 可串行化 s x x + ``` + + - 共享锁(s) + - 排它锁(x) + + - 间隙锁(一段数据,不加锁会导致事物幻读) + + ``` + InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁! + ``` + +##### 4.3事物隔离级别-可重复读(幻读,引入mvcc) + +``` +对于innodb引擎, +MVCC(MultiVersion Concurrency Control)是通过在每行记录后面保存两个隐藏的列来实现的。 +这两个列,一个保存了行的创建系统版本号,一个保存行的删除系统版本号。 +每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。 +``` + +- select + + ``` + 创建版本早于当前事务版本号的数据行(读取不到之后创建的数据) + 删除版本要么未定义,要么大于当前事务版本号。(可以确保事务读取到的行,在事务开始之前未被删除) + ``` + +- insert + + ``` + InnoDB为新插入的每一行保存当前系统版本号作为行版本号。 + ``` + +- delete + + ``` + InnoDB为删除的每一行保存当前系统版本号作为行删除标识。 + ``` + +- update + + ``` + InnoDB插入一行新记录,保存当前系统版本号作为行版本号 + 同时将原行的行删除标识保存当前系统版本号 + ``` + +- 参考:https://www.jianshu.com/p/f692d4f8a53e + +#### 5. 主从同步 + +##### 5.1 redo,undo,binlog + +redo存在于innodb,并记录的是物理数据,多次执行具有幂等性 + +binlog存在于mysql server, 记录的为逻辑数据。 + +``` +client--> update memory data -->indb redo(prepare)--> binlog -->commit(redo commit) +``` + +
+ +
+ +``` +参考: +详细分析MySQL事务日志(redo log和undo log) +https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.html +``` diff --git "a/markdown/2020/20200520-\350\257\273\344\271\246\347\254\224\350\256\260-\351\242\206\345\237\237\351\251\261\345\212\250.md" "b/markdown/2020/20200520-\350\257\273\344\271\246\347\254\224\350\256\260-\351\242\206\345\237\237\351\251\261\345\212\250.md" new file mode 100644 index 0000000..1add407 --- /dev/null +++ "b/markdown/2020/20200520-\350\257\273\344\271\246\347\254\224\350\256\260-\351\242\206\345\237\237\351\251\261\345\212\250.md" @@ -0,0 +1,143 @@ +[TOC] + +### 领域驱动笔记 + +> [实现领域驱动] vaughn vernon + +#### 1.领域概念 + +对于业务的解决方案,通常采用抽象,分治,知识。 + +领域包含问题域和解空间: + +- 问题域可细分为核心域,通用域,支撑域。由领域专家,开发者,测试等人员共同参与**通用语言**的制定,用于描述问题域。 + +- 解空间不同的**限界上下文**组成,通过**上下文映射图**表示各上下文之间的关系,一个限界上下文指一个解决方案,包含一个子域。 + +限界上下文:用于封装通用语言和领域对象,标定了业务服务系统的边界。包含领域服务,领域事件,实体,值对象,聚合根,资源库。可以通过门面模式与其他上下文交互。 + +限界上下文之间交互: 通过开放主机模式,比如REST,RPC接口,但ddd推荐上游使用异步通知来达到服务自治.p92 + +#### 2.架构 + +一个上下文对应一个领域架构,包括接口层,应用层,领域层p116 + +领域架构方案如下: + +(1) 分层架构 + +(2) DIP依赖翻转架构 + +(3) 六边形架构 + +- 支持http,RPC, mq等上游端口模式,或者下游资源库依赖关系型,文档型,内存型等组件 + +(4) SOA面向服务架构 + +- 业务价值高于技术策略p116 +- 战略目标高于项目利益 + +(5) 事件驱动架构 + +- 串行化处理,以管道流执行 + +(6)长时处理架构 + +- 串行化+并行化处理,事件开始及结尾成环状 + +(7)事件源架构 + +- 快照+日志重放 + +#### 3.实体,值对象 + +领域对象包含实体,值对象,聚合根。 + +(1)实体具有唯一标识和可变性,实体唯一标识生成方式: + +- 用户填写 +- uuid +- 持久化生成(mysql,redis) + +> **通用语言**提供了设计领域模型时的概念术语。根据通用语言分析实体概念时,要避免技术和战术化,从战略层面进行分析。p168 + +(2 )值对象是稳定的,所以值对象可以存放实体的唯一标识p151 + +值对象的特征: + +- 度量或描述 +- 不变性 +- 整体概念 +- 可替换、值对象相等、无副作用行为 + +使用值对象进行最小化集成,定义基础值对象,让继承值对象直接使用 p206 + +- 如*Collaborator* + +使用值对象表示标准类型,定义描述或枚举,让其成为标准对象 p207 + +- 如*GroupMemberType* + +拒绝由数据建模泄露带来的不利影响,根据领域模型来设计数据模型,不应颠倒p221 + +所以应从领域模型的角度,考虑使用实体或者**值对象(建议)**来表述数据库中的数据 + +#### 4.领域服务 + +>业务中这样一种场景:实体和值对象有各自的职责,但某些操作过程并不是实体或者值对象的行为时,那最佳位置在哪儿? + +领域服务和通用语言是一致的,并且是无状态的。p237 + +过渡的使用领域服务,会导致**贫血领域模型**,即所有的业务逻辑都位于领域服务中,而不是实体或者值对象。 + +领域服务可根据实际情况,无需预先定义独立接口,语义化的实现具体服务即可。 + +#### 5.领域事件 + +(1) 领域事件的发布及订阅: + +- 订阅: 实施者一般为**应用服务**,或者领域服务。订阅逻辑中的事务原子性可通过调用**应用服务**实现p262 +- 发布: 实施者为聚合,实体内的状态变更,而事件发布工具并没有领域的概念。p265 + +(2) 向远程限界上下文发送领域事件的特征: + +- 确保消息一致性 +- 可自治 +- 存在延时性 + +(3) 转发储存风格架构 + +- REST: 是一种拉取风格。领域模型中产生事件,由客户端进行拉取。服务端,客户端可以适当进行缓存策略。客户端自身按需进行序列判断、过滤等。参考promethues监控打点 +- MQ: 是一种推送风格。MQ组件保证消息的有序性。 + +#### 6.聚合 + +(1) 设计小聚合,并通过唯一标识引用其他聚合 p322 + +(2) 在聚合内部可以使用**领域事件** p327 + +(3) 原则上单事务修改单个聚合实例,但是可以打破规则,单事务修改多个聚合实例。 p331 + + + + + +#### 编码参考 + +``` + application + application.eventStore #应用组件 p288 + domain + domain.model #领域模型 + domain.model.aggregate聚合根 p316 + infrastructure + infrastructure.services # p242 + infrastructure.repository + infrastructure.component + lib + lib.logger +``` + + + + \ No newline at end of file diff --git "a/markdown/2020/20200609-go\347\250\213\345\272\217\347\253\236\346\200\201\345\217\212\346\200\247\350\203\275\345\210\206\346\236\220.md" "b/markdown/2020/20200609-go\347\250\213\345\272\217\347\253\236\346\200\201\345\217\212\346\200\247\350\203\275\345\210\206\346\236\220.md" new file mode 100644 index 0000000..11f0096 --- /dev/null +++ "b/markdown/2020/20200609-go\347\250\213\345\272\217\347\253\236\346\200\201\345\217\212\346\200\247\350\203\275\345\210\206\346\236\220.md" @@ -0,0 +1,41 @@ +#### go程序分析 + + + +##### 1.竞态条件 + +在编译时添加`-race`,程序在运行期间,可以查看是否存在竞态条件。 + +``` +#参考: https://segmentfault.com/a/1190000020107431 +$ go test -race mypkg +$ go run -race mysrc.go +$ go build -race mycmd +$ go install -race mypkg +``` + +通过竞态条件,来分析在多核处理器时,多个goroutine并发访问锁,全局变量,内存时是否存在未知问题。 + +##### 2.运行时性能问题 + +对于go http pprof包: + +1.goroutine: + +```json +#pprof: http:port/debug/pprof/ +# debug=2 查看更详细的信息 +/pprof/goroutine?debug=2 可以查看详细的goroutine状态,锁等待,耗时等 + +# https://juejin.im/entry/5ac9cf3a518825556534c76e +goroutine: 获取程序当前所有 goroutine 的堆栈信息。 +heap: 包含每个 goroutine 分配大小,分配堆栈等。每分配 runtime.MemProfileRate(默认为512K) 个字节进行一次数据采样。 +threadcreate: 获取导致创建 OS 线程的 goroutine 堆栈 +block: 获取导致阻塞的 goroutine 堆栈(如 channel, mutex 等),使用前需要先调用 runtime.SetBlockProfileRate +mutex: 获取导致 mutex 争用的 goroutine 堆栈,使用前需要先调用 runtime.SetMutexProfileFraction +``` + + + + + diff --git "a/markdown/2020/20201011-linux\350\277\233\347\250\213io\347\243\201\347\233\230\346\200\247\350\203\275\345\210\206\346\236\220.md" "b/markdown/2020/20201011-linux\350\277\233\347\250\213io\347\243\201\347\233\230\346\200\247\350\203\275\345\210\206\346\236\220.md" new file mode 100644 index 0000000..3343470 --- /dev/null +++ "b/markdown/2020/20201011-linux\350\277\233\347\250\213io\347\243\201\347\233\230\346\200\247\350\203\275\345\210\206\346\236\220.md" @@ -0,0 +1,99 @@ +### linux 进程io磁盘性能分析 + +**问题: 当前linux运行了某个进程,如何查看改进程对磁盘io的消耗情况?** + +[TOC] + +##### 步骤1.查看运行进程io文件路径 + +```shell +#系统命令lsof +> lsof -p 3861 # 3861为该进程pid +COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME +... +influxd 3861 root mem REG 253,4 2147809858 102629460 /data/influxdb/data/ +... +``` + +可以看到进程io的路径为: `/data` + +##### 步骤2.查看磁盘io状态 + +```shell +#系统命令iostat +> iostat -x 1 3 #每秒打印1次,打印3次磁盘状态 +#示例第二次状态 +avg-cpu: %user %nice %system %iowait %steal %idle + 7.98 0.00 3.80 10.33 0.00 77.89 + +Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util +sda 26.00 1480.00 2544.00 1692.00 28800.00 33576.00 29.45 4.18 0.98 1.59 0.06 0.23 98.70 +dm-0 0.00 0.00 316.00 0.00 1384.00 0.00 8.76 0.42 1.36 1.36 0.00 0.88 27.90 +dm-4 0.00 0.00 2198.00 3163.00 27192.00 33576.00 22.67 3.48 0.64 1.48 0.06 0.18 97.50 +``` + +以Device: `dm-4`为例: + +- 当前磁盘iops为5361/s (r + w) +- 每秒io读取约27M/s, 写入约33M/s +- io队列中,有3.48个堆积 (avgqu-sz) +- 每次io等待 0.47ms (await), 处理耗时0.17ms (svctm) +- %util为97.5%,接近100%,说明I/O请求太多,I/O系统已经满负荷 + +> 如何确定哪个Device为该进程所用: 是sda ? dm-0 ? 还是dm-4? + +##### 步骤3.查看磁盘分区 + +```shell +#系统命令lsblk +> lsblk +NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT +sda 8:0 0 1.6T 0 disk #TYPE=disk,物理盘 +├─sda1 8:1 0 1G 0 part /boot #TYPE=part,分区 +└─sda2 8:2 0 1.6T 0 part + ├─VolGroup00-LogVol03 253:0 0 50G 0 lvm / #TYPE=lvm, 逻辑卷 + ├─VolGroup00-LogVol00 253:1 0 31.3G 0 lvm [SWAP] + ├─VolGroup00-LogVol02 253:2 0 10G 0 lvm /var + ├─VolGroup00-LogVol01 253:3 0 2G 0 lvm /tmp + └─VolGroup00-LogVol04 253:4 0 1.6T 0 lvm /data #当前进程io的逻辑卷 +``` + +sda为物理磁盘,有sda1和sda2两个分区。sda2分区下有5个lvm **(Logical Volume Manager)** 卷: + +> LVM:通过将底层物理硬盘抽象封装,以逻辑卷的形式表现给上层系统,逻辑卷的大小可以动态调整,而且不会丢失现有数据。新加入的硬盘也不会改变现有上层逻辑卷。 + +##### 步骤4.查看文件系统及挂载点 + +```shell +#系统命令df -h +> df -h +Filesystem Size Used Avail Use% Mounted on +/dev/mapper/VolGroup00-LogVol03 50G 3.1G 47G 7% / +devtmpfs 32G 0 32G 0% /dev +tmpfs 32G 0 32G 0% /sys/fs/cgroup +/dev/sda1 1014M 167M 848M 17% /boot +/dev/mapper/VolGroup00-LogVol01 2.0G 33M 2.0G 2% /tmp +/dev/mapper/VolGroup00-LogVol02 10G 401M 9.6G 4% /var +/dev/mapper/VolGroup00-LogVol04 1.6T 452G 1.0T 31% /data +``` + +可以看到 `/data`的文件系统路径为:**/dev/mapper/VolGroup00-LogVol04** + +##### 步骤5.查看进程对应`iotstat`的Device + +```shell +#进入步骤4中的路径:/dev/mapper +> cd /dev/mapper/ +#查看当前文件映射 +> ll +total 0 +crw------- 1 root root 10, 236 Sep 8 15:00 control +lrwxrwxrwx 1 root root 7 Sep 8 15:00 VolGroup00-LogVol00 -> ../dm-1 +lrwxrwxrwx 1 root root 7 Sep 8 15:00 VolGroup00-LogVol01 -> ../dm-3 +lrwxrwxrwx 1 root root 7 Sep 8 15:00 VolGroup00-LogVol02 -> ../dm-2 +lrwxrwxrwx 1 root root 7 Sep 8 15:00 VolGroup00-LogVol03 -> ../dm-0 +lrwxrwxrwx 1 root root 7 Sep 8 16:54 VolGroup00-LogVol04 -> ../dm-4 #映射dm-4 +``` + +所以该进程在`iostat`所对应的Device为**dm-4**, dm-4从属于**sda物理盘**。 + diff --git "a/markdown/2020/20201028-influx\345\206\205\345\255\230\346\266\210\350\200\227\345\210\206\346\236\220\345\217\212\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/markdown/2020/20201028-influx\345\206\205\345\255\230\346\266\210\350\200\227\345\210\206\346\236\220\345\217\212\346\200\247\350\203\275\344\274\230\345\214\226.md" new file mode 100644 index 0000000..c72632f --- /dev/null +++ "b/markdown/2020/20201028-influx\345\206\205\345\255\230\346\266\210\350\200\227\345\210\206\346\236\220\345\217\212\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -0,0 +1,284 @@ +### influxdb内存消耗分析及性能优化 + +[TOC] + +#### 1.问题现象 + +由于业务场景需求,在生产环境服务器(32core64G)搭建了基于golang开发的influx时序数据库[v1.8版本](https://portal.influxdata.com/downloads/) ,经过持续一周的运行之后(每天写入约100G数据),发现服务器内存消耗95%以上,并偶现`SWAP`报警: + +> **(swap使用率)\[交换内存使用率\]\[79.10744\][server_alarm]** + + 使用`top`命令查看当前服务器状态: + +```shell +top - 16:06:48 up 31 days, 1:03, 4 users, load average: 0.01, 0.11, 0.30 +Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie +%Cpu(s): 45.0 us, 3.0 sy, 0.0 ni, 37.9 id, 43.1 wa, 0.0 hi, 0.0 si, 0.0 st +KiB Mem : 65433636 total, 274968 free, 63048048 used, 2110620 buff/cache +KiB Swap: 32833532 total, 30839420 free, 1994112 used. 1776336 avail Mem + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND +32309 root 20 0 0.411t 0.058t 177080 S 1534 95.3 6926:00 influxd +``` + +influx进程物理内存占用58G, 内存使用率95.3%;且当前wa为43.1,说明磁盘IO非常繁忙。于是便思考: + +- 为什么进程内存消耗那么高? +- 为什么磁盘io那么忙? + +#### 2.原因分析 + +##### 2.1 内存高消耗分析 + +(1) 使用influx客户端, 查看influx服务的`runtime`状态: + +```shell +> ./influx -host 10.xx.xx.xx -port x -username 'x' -password 'x' -execute "show stats" +name: runtime +Alloc Frees HeapAlloc HeapIdle HeapInUse HeapObjects HeapReleased HeapSys Lookups Mallocs NumGC NumGoroutine PauseTotalNs Sys TotalAlloc +----- ----- --------- -------- --------- ----------- ------------ ------- ------- ------- ----- ------------ ------------ --- ---------- +16389315856 363815829 16389315856 51905806336 16609361920 254434947 44391612416 68515168256 0 618250776 2336 24 15652952340 71222090601 45846325521880 + +name: database +tags: database=xxx +numMeasurements numSeries +--------------- --------- +3 20927158 +``` + +发现当前influxd进程`HeapIdle`约51G, `HeapInUse`约16G,`HeapReleased`约44G, 当前series数量为2092万左右.随之而来的疑惑: + +> **为什么进程RES实际占用58G, 而当前进程runtime堆占用内存仅有23G ???** +> +> (HeapIdle)51-(HeapReleased)44+(HeapInUse)16 = 23 G + +(2) 为了确认是否存在内存泄漏,进一步查看进程的内存块详细数据: + +```shell +#当前influxd进程id为32309 +#1.pmap命令查看进程内存块分配 +> pmap -x 32309 | less +32309: /etc/influxdb/usr/bin/influxd -config /etc/influxdb/influxdb.conf +Address Kbytes RSS Dirty Mode Mapping +0000000000400000 14928 2284 0 r-x-- influxd +0000000001294000 31092 3664 0 r---- influxd +00000000030f1000 4668 4360 368 rw--- influxd +0000000003580000 180 96 96 rw--- [ anon ] +0000000004ead000 132 0 0 rw--- [ anon ] +000000c000000000 66912256 59789960 51476676 rw--- [ anon ] #堆内存 +00007f80e6469000 4232 1440 1440 rw--- [ anon ] +00007f80e6913000 1886000 1872676 1872672 rw--- [ anon ] +00007f8159ae5000 232264 230124 230124 rw--- [ anon ] +00007f8167dc3000 172360 168804 168804 rw--- [ anon ] +00007f817261c000 111564 107452 107452 rw--- [ anon ] + +#2.查看更详细的每一块内存分配 +#命令:cat /proc/pid/smaps +#如下发现进程堆内存地址空间为:c000000000-cff4000000 +> cat /proc/32309/smaps | less +c000000000-cff4000000 rw-p 00000000 00:00 0 +Size: 66912256 kB +Rss: 59789960 kB +Pss: 59789960 kB +Shared_Clean: 0 kB +Shared_Dirty: 0 kB +Private_Clean: 8313284 kB +Private_Dirty: 51476676 kB +Referenced: 51368732 kB +Anonymous: 59789960 kB +AnonHugePages: 5994496 kB +Swap: 1055452 kB +KernelPageSize: 4 kB +MMUPageSize: 4 kB +Locked: 0 kB +VmFlags: rd wr mr mp me ac sd + +#3.使用gdb打印堆栈 +#输入程序地址空间0xc000000000 0xcff4000000 +> gdb -p 32309 +>>> dump binary memory ./meminfo.log 0xc000000000 0xcff4000000 +>>> bt #查看内存调用栈backtrace +>>> q #退出 + +#4.查看内存内容meminfo.log +hexdump -C ./meminfo.log | less #查看内存块数据 +``` + +通过内存块调用栈`bt`命令及导出的`meminfo.log`文件,并没有发现内存泄漏的导向。 + +(3) 使用 **`go pprof`** 查看进程累计内存分配`alloc-space` + +```go +> go tool pprof -alloc_space http://host:port/debug/pprof/heap +Fetching profile over HTTP from http://host:port/debug/pprof/heap +Saved profile in /home/yushaolong/pprof/pprof.influxd.alloc_objects.alloc_space.inuse_objects.inuse_space.004.pb.gz +File: influxd +Type: alloc_space +Time: Oct 9, 2020 at 3:59pm (CST) +Entering interactive mode (type "help" for commands, "o" for options) +(pprof) top +Showing nodes accounting for 42527GB, 99.59% of 42700.66GB total +Dropped 443 nodes (cum <= 213.50GB) + flat flat% sum% cum cum% + 42527GB 99.59% 99.59% 42527GB 99.59% github.com/influxdata/influxdb/tsdb/index/inmem.(*Index).DropSeriesGlobal /go/src/github.com/influxdata/influxdb/tsdb/index/inmem/inmem.go + 0 0% 99.59% 42528.02GB 99.60% github.com/influxdata/influxdb/services/retention.(*Service).Open.func1 /go/src/github.com/influxdata/influxdb/services/retention/service.go + 0 0% 99.59% 42527.94GB 99.60% github.com/influxdata/influxdb/tsdb.(*Store).DeleteShard.func3 /go/src/github.com/influxdata/influxdb/tsdb/store.go +(pprof) list DropSeriesGlobal +Total: 42700.66GB +ROUTINE ======================== github.com/influxdata/influxdb/tsdb/index/inmem.(*Index).DropSeriesGlobal in /go/src/github.com/influxdata/influxdb/tsdb/index/inmem/inmem.go + 42527GB 42527GB (flat, cum) 99.59% of Total + . . 792: } + . . 793: + . . 794: i.mu.Lock() + . . 795: defer i.mu.Unlock() + . . 796: + 42527GB 42527GB 797: k := string(key) + . . 798: series := i.series[k] + . . 799: if series == nil { + . . 800: return nil + . . 801: } + . . 802: +(pprof) +``` + +发现进程在删除`series(influx索引)`时, 累计消耗了42T的内存空间。说明进程在series删除时消耗了大量的内存堆(https://github.com/influxdata/influxdb/issues/10453) , 所以占用内存会在此时持续飙高,但这些内存应该会被GC掉?重新看一下runtime及系统内存分配,发现了一些端倪: + +| 进程RES | HeapIdle | HeapReleased | HeapInUse | +| :-----: | :------: | :----------: | :-------: | +| 58G | 51G | 44G | 16G | + +目前influxd进程持有的有效内存为 51-44+16=23G, 而系统进程RES为58G。**猜想存在58-23=35g的内存,进程标记不再使用,当然系统也没有进行回收。** + +(4) 使用`memtester`工具验证猜想: + +```shell +# 内存测试工具 memtester +# 使用文档:https://www.cnblogs.com/xiayi/p/9640619.html +# 向操作系统申请 30G内存 +> /usr/local/bin/memtester 30G 1 +memtester version 4.5.0 (64-bit) +Copyright (C) 2001-2020 Charles Cazabon. +Licensed under the GNU General Public License version 2 (only). + +pagesize is 4096 +pagesizemask is 0xfffffffffffff000 +want 20480MB (21474836480 bytes) +got 20480MB (21474836480 bytes), trying mlock ...locked. +Loop 1/1: + Stuck Address : setting 1 +``` + +向操作系统申请30G内存后,使用`top`命令查看内存状态: + +```shell +top - 16:24:13 up 31 days, 1:21, 5 users, load average: 1.28, 1.21, 0.70 +Tasks: 386 total, 2 running, 384 sleeping, 0 stopped, 0 zombie +%Cpu(s): 3.1 us, 0.0 sy, 0.0 ni, 96.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st +KiB Mem : 65433636 total, 820012 free, 63739976 used, 873648 buff/cache +KiB Swap: 32833532 total, 30837852 free, 1995680 used. 1174164 avail Mem + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND +32309 root 20 0 0.411t 0.029t 8764 S 0.0 48.1 6946:31 influxd +14921 root 20 0 30.004g 0.029t 464 R 99.7 48.1 3:25.59 memtester +``` +此时,influxd占用29G内存,memtester占用29G内存。**果然influxd释放的内存,此时才被系统重新回收**, 翻阅了go的资料,找到了原因(https://colobu.com/2019/08/28/go-memory-leak-i-dont-think-so/): + +> 一直以来 go 的 runtime 在释放内存返回到内核时,在 Linux 上使用的是 MADV_DONTNEED,虽然效率比较低,但是会让 RSS(resident set size 常驻内存集)数量下降得很快。不过在 go 1.12 里专门针对这个做了优化,runtime 在释放内存时,使用了更加高效的 MADV_FREE 而不是之前的 MADV_DONTNEED。这样带来的好处是,一次 GC 后的内存分配延迟得以改善,runtime 也会更加积极地将释放的内存归还给操作系统,以应对大块内存分配无法重用已存在的堆空间的问题。不过也会带来一个副作用:RSS 不会立刻下降,而是要等到系统有内存压力了,才会延迟下降。为了避免像这样一些靠判断 RSS 大小的自动化测试因此出问题,也提供了一个 GODEBUG=madvdontneed=1 参数可以强制 runtime 继续使用 MADV_DONTNEED。 + +原来是由于go内部优化而使进程内存没有立即释放,至此解答了内存高消耗的疑惑。 + +##### 2.2 磁盘io消耗分析 + +使用`iostat`命令查看磁盘io状态: + +```shell +#系统命令iostat +> iostat -x 1 3 #每秒打印1次,打印3次磁盘状态 +#示例第二次状态 +avg-cpu: %user %nice %system %iowait %steal %idle + 7.98 0.00 3.80 10.33 0.00 77.89 + +Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util +sda 26.00 1480.00 2544.00 1692.00 28800.00 33576.00 29.45 4.18 0.98 1.59 0.06 0.23 98.70 +dm-0 0.00 0.00 316.00 0.00 1384.00 0.00 8.76 0.42 1.36 1.36 0.00 0.88 27.90 +dm-4 0.00 0.00 2198.00 3163.00 27192.00 33576.00 22.67 3.48 0.64 1.48 0.06 0.18 97.50 +``` + +按照 [linux进程io磁盘性能分析](https://github.com/alwaysthanks/learning-docs/blob/master/markdown/2020/20201011-linux%E8%BF%9B%E7%A8%8Bio%E7%A3%81%E7%9B%98%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90.md) ,得知influxd进程写盘的Device为`dm-4`,指标分析如下: + +- 当前磁盘iops为5361/s (r + w) +- 每秒io读取约27M/s, 写入约33M/s +- io队列中,有3.48个堆积 (avgqu-sz) +- 每次io等待 0.47ms (await), 处理耗时0.17ms (svctm) +- %util为97.5%,接近100%,说明I/O请求太多,I/O系统已经满负荷 + +发现influx进程对磁盘的io消耗过大。 + +#### 3.性能优化 + +通过以上分析,可以得到: + +- influx使用`inmem`引擎时(默认),在retention policy时会消耗过高的内存 +- 使用`GODEBUG=madvdontneed=1`可以让go程序尽快释放内存 +- influx磁盘的iops过高,应该从(增大内存buffer/增加批量写落盘)方面进行优化 + +因此对配置文件`influxdb.conf`做了如下优化: + +```toml +# 详细配置说明见官方文档 +# https://docs.influxdata.com/influxdb/v1.8/administration/config/#data-settings + +[data] + #说明: wal预写日志log,用于事务一致性 + #默认为0,每次写入都落盘。 + #修改为3s, 根据业务场景,不保证强一致性,可采用异步刷盘 + #[优化点]:用于减轻磁盘io压力 + wal-fsync-delay = "3s" + + #说明: influx索引引擎 + #默认为inmem,创建内存型索引,在delete retention会消耗过高内存 + #修改为tsi1, 注意重建ts1索引(https://blog.csdn.net/wzy_168/article/details/107043840) + #[优化点]:降低删除保留策略时的内存消耗 + index-version = "tsi1" + + #说明: 从内存压缩分片TSM数据落盘 + #默认为4h + #修改为1h + #[优化点]: 平衡cpu压力,将4h的检测周期分散到1小时。同时分散磁盘io压力 + compact-full-write-cold-duration = "1h" + + #说明: 压缩TSM数据,一次落盘的吞吐量 + #默认48m + #修改为64m + #[优化点]:增大写入量,减轻io压力 + compact-throughput = "64m" +``` + +修改配置之后,执行如下命令启动influx进程: + +```shell +env GODEBUG=madvdontneed=1 /usr/bin/influxd -config /usr/bin/influxdb.conf +``` + +#### 4.线上验证 + +influxd进程重新运行一周之后,再次观察系统状态: + +(1) 内存消耗约占55%左右: + +
+ +
+ +(2) 磁盘iops约为200左右,util占用6.2%。 + +```shell +avg-cpu: %user %nice %system %iowait %steal %idle + 7.21 0.00 1.00 0.16 0.00 91.64 + +Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util +sda 0.00 39.00 5.00 161.00 20.00 28036.00 338.02 0.25 1.51 10.40 1.24 0.37 6.10 +dm-4 0.00 0.00 5.00 189.00 20.00 28036.00 289.24 0.26 1.32 10.60 1.07 0.32 6.20 +``` + +发现进程运行符合预期,问题得到解决。 \ No newline at end of file diff --git "a/markdown/2020/20201210-golang\346\261\207\347\274\226\345\210\235\346\216\242.md" "b/markdown/2020/20201210-golang\346\261\207\347\274\226\345\210\235\346\216\242.md" new file mode 100644 index 0000000..c447de1 --- /dev/null +++ "b/markdown/2020/20201210-golang\346\261\207\347\274\226\345\210\235\346\216\242.md" @@ -0,0 +1,134 @@ +### golang汇编初探 + +##### 1.抛出问题 + + 如下一段代码执行后,基于go1.10.8, 输出结果为: 10,原因是什么? + +```go +package main + +import "fmt" + +func main() { + + val := deferValue2() + ret := val + fmt.Println(ret) +} + +func deferValue2() int { + var a = 10 + defer func(i *int) { + *i = 3 + }(&a) + return a +} +``` + +##### 2.汇编代码 + +通过打印程序汇编代码,看看究竟发生了什么: + +```shell +# 打印汇编代码命令 +go tool compile -S -N -l defer.go +``` + +汇编代码如下: + +```shell +#main函数入口 +"".main STEXT size=272 args=0x0 locals=0x90 + 0x0000 00000 (defer.2.go:5) TEXT "".main(SB), $144-0 + 0x001f 00031 (defer.2.go:5) SUBQ $144, SP # sp移到栈顶 + 0x0026 00038 (defer.2.go:5) MOVQ BP, 136(SP) # 将上层调用寄存器保存到sp136 + 0x002e 00046 (defer.2.go:5) LEAQ 136(SP), BP # LEAQ用于赋值调用地址, 即BP指向该函数栈的136 + 0x0036 00054 (defer.2.go:7) CALL "".deferValue2(SB) #调用deferValue2函数 + 0x003b 00059 (defer.2.go:7) MOVQ (SP), AX # 因为没有传参,只有返回值,所以返回值地址为sp-sp8 + 0x003f 00063 (defer.2.go:7) MOVQ AX, "".val+48(SP) + 0x0044 00068 (defer.2.go:8) MOVQ AX, "".ret+56(SP) + ... + ... +#deferValue2函数入口 +"".deferValue2 STEXT size=177 args=0x8 locals=0x28 + 0x0000 00000 (defer.2.go:12) TEXT "".deferValue2(SB), $40-8 #申请40字节占空间,8字节返回值 + 0x0000 00000 (defer.2.go:12) MOVQ TLS, CX + 0x0009 00009 (defer.2.go:12) MOVQ (CX)(TLS*2), CX + 0x0010 00016 (defer.2.go:12) CMPQ SP, 16(CX) + 0x0014 00020 (defer.2.go:12) JLS 167 + 0x001a 00026 (defer.2.go:12) SUBQ $40, SP #sp移到栈顶 + 0x001e 00030 (defer.2.go:12) MOVQ BP, 32(SP) #将上次BP寄存器信息保存到sp32栈 + 0x0023 00035 (defer.2.go:12) LEAQ 32(SP), BP #BP指向sp32栈 + 0x0028 00040 (defer.2.go:12) FUNCDATA $0, gclocals·263043c8f03e3241528dfae4e2812ef4(SB) + 0x0028 00040 (defer.2.go:12) FUNCDATA $1, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB) + 0x0028 00040 (defer.2.go:12) MOVQ $0, "".~r0+48(SP) #48sp为上层main函数栈顶 + 0x0031 00049 (defer.2.go:13) LEAQ type.int(SB), AX #AX初始化为int类型 + 0x0038 00056 (defer.2.go:13) MOVQ AX, (SP) #栈底sp=0 + 0x003c 00060 (defer.2.go:13) PCDATA $0, $0 + 0x003c 00060 (defer.2.go:13) CALL runtime.newobject(SB) #申请堆空间 + 0x0041 00065 (defer.2.go:13) MOVQ 8(SP), AX #sp8指返回值,即堆栈指针0xabc, newobject 有入参和返回值 + 0x0046 00070 (defer.2.go:13) MOVQ AX, "".&a+24(SP) # sp24=0xabc + 0x004b 00075 (defer.2.go:13) MOVQ $10, (AX) # *AX=10,即堆空间值为10 + 0x0052 00082 (defer.2.go:16) MOVQ "".&a+24(SP), AX # AX=0xabc + 0x0057 00087 (defer.2.go:16) MOVQ AX, 16(SP) # sp16=0xabc + 0x005c 00092 (defer.2.go:14) MOVL $8, (SP) # sp0 低位存储值 8 + 0x0063 00099 (defer.2.go:14) LEAQ "".deferValue2.func1·f(SB), AX # AX指向匿名函数fn + 0x006a 00106 (defer.2.go:14) MOVQ AX, 8(SP) # sp8=fn + 0x006f 00111 (defer.2.go:14) PCDATA $0, $1 + 0x006f 00111 (defer.2.go:14) CALL runtime.deferproc(SB) # 将fn匿名函数加入defer链表 + 0x0074 00116 (defer.2.go:16) TESTL AX, AX + 0x0076 00118 (defer.2.go:16) JNE 151 + 0x0078 00120 (defer.2.go:16) JMP 122 + 0x007a 00122 (defer.2.go:17) MOVQ "".&a+24(SP), AX # AX=0xabc + 0x007f 00127 (defer.2.go:17) MOVQ (AX), AX # AX=*AX = 10, 注意此处值拷贝 + 0x0082 00130 (defer.2.go:17) MOVQ AX, "".~r0+48(SP) # sp48=10, 即main栈顶为10 + 0x0087 00135 (defer.2.go:17) PCDATA $0, $0 + 0x0087 00135 (defer.2.go:17) XCHGL AX, AX + 0x0088 00136 (defer.2.go:17) CALL runtime.deferreturn(SB) # defer链表pop, 执行fn函数 + 0x008d 00141 (defer.2.go:17) MOVQ 32(SP), BP # 返回寄存器信息 + 0x0092 00146 (defer.2.go:17) ADDQ $40, SP # 清空栈底 + 0x0096 00150 (defer.2.go:17) RET # 返回上一级 + 0x0097 00151 (defer.2.go:14) PCDATA $0, $0 + 0x0097 00151 (defer.2.go:14) XCHGL AX, AX + 0x0098 00152 (defer.2.go:14) CALL runtime.deferreturn(SB) + 0x009d 00157 (defer.2.go:16) MOVQ 32(SP), BP + 0x00a2 00162 (defer.2.go:16) ADDQ $40, SP + 0x00a6 00166 (defer.2.go:16) RET + 0x00a7 00167 (defer.2.go:16) NOP + 0x00a7 00167 (defer.2.go:12) PCDATA $0, $-1 + 0x00a7 00167 (defer.2.go:12) CALL runtime.morestack_noctxt(SB) + 0x00ac 00172 (defer.2.go:12) JMP 0 + ... + ... +#匿名函数入口 +"".deferValue2.func1 STEXT nosplit size=15 args=0x8 locals=0x0 + 0x0000 00000 (defer.2.go:14) TEXT "".deferValue2.func1(SB), NOSPLIT, $0-8 + 0x0000 00000 (defer.2.go:14) FUNCDATA $0, gclocals·a36216b97439c93dafebe03e7f0808b5(SB) + 0x0000 00000 (defer.2.go:14) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) + 0x0000 00000 (defer.2.go:15) MOVQ "".i+8(SP), AX #函数入参,即AX=0xabc, 由于在runtime.deferreturn执行,所以sp不相对以上 + 0x0005 00005 (defer.2.go:15) TESTB AL, (AX) + 0x0007 00007 (defer.2.go:15) MOVQ $3, (AX) # *AX = 3, 修改了0xabc指向堆空间的值为3 + 0x000e 00014 (defer.2.go:16) RET # 返回 + #... + #... +``` + +##### 3.分析原因 + +通过第2部分观察,可以发现: + +```shell +#此处进行值拷贝,并返回上层调用栈顶 +0x007f 00127 (defer.2.go:17) MOVQ (AX), AX # AX=*AX = 10, 注意此处值拷贝 +0x0082 00130 (defer.2.go:17) MOVQ AX, "".~r0+48(SP) # sp48=10, 即main栈顶为10 +``` + +所以返回值为10. + +##### 4.附图 + +为什么函数栈的栈底需要+8个字节,因为存在**返回地址**,之上才是传参和返回值。 + + + +![control-flow](https://github.com/alwaysthanks/learning-docs/blob/master/images/20201210-callstack.png) \ No newline at end of file diff --git "a/markdown/2020/20201211-linux\345\270\270\347\224\250\346\200\247\350\203\275\345\210\206\346\236\220\345\221\275\344\273\244.md" "b/markdown/2020/20201211-linux\345\270\270\347\224\250\346\200\247\350\203\275\345\210\206\346\236\220\345\221\275\344\273\244.md" new file mode 100644 index 0000000..f614b27 --- /dev/null +++ "b/markdown/2020/20201211-linux\345\270\270\347\224\250\346\200\247\350\203\275\345\210\206\346\236\220\345\221\275\344\273\244.md" @@ -0,0 +1,251 @@ +### linux常用性能分析命令 + +[TOC] + +#### 1. top + +```shell +top - 16:06:48 up 31 days, 1:03, 4 users, load average: 0.01, 0.11, 0.30 +Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie +%Cpu(s): 45.0 us, 3.0 sy, 0.0 ni, 37.9 id, 43.1 wa, 0.0 hi, 0.0 si, 0.0 st +KiB Mem : 65433636 total, 274968 free, 63048048 used, 2110620 buff/cache +KiB Swap: 32833532 total, 30839420 free, 1994112 used. 1776336 avail Mem + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND +32309 root 20 0 0.411t 0.058t 177080 S 1534 95.3 6926:00 influxd +``` + +`top`命令需要留意几个指标: + +- **load average: 0.01, 0.11, 0.30** : 表示最近1,5,15分钟机器负载统计,当大于1,表示超过了机器的负载能力. 越小越好,建议不超过0.7 +- **43.1 wa, 0.0 hi, 0.0 si** : wa表示io压力,hi表示硬中断, si表示软中断。wa一般应等于0,可以看出目前io压力很大 +- **VIRT RES SHR**: VIRT表示进程虚拟空间,RES表示进程物理空间,SHR表示共享空间,一般用 **(RES-SHR)** 表示进程占用的物理内存。本例可看到进程占用了58g的物理内存 + +#### 2. sar + +`sar`命令是linux上非常强大的系统监控命令,能够监控io,cpu,带宽等: + +```shell +#检查网卡带宽流量,-n表示network, DEV表示设备,1 3 表示1s输出一次,总共输出3次 +#可以看到eth1网卡,收报305kB/s, 发送203kB/s +> sar -n DEV 1 3 + +03:01:16 PM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s +03:01:17 PM lo 477.55 477.55 70.84 70.84 0.00 0.00 0.00 +03:01:17 PM eth1 1385.71 1451.02 305.37 203.73 0.00 0.00 0.00 +``` + +#### 3. ethtool + +`ethtool`能够检查及设置以太网卡状态: + +```shell +# 查看网卡状态 +> ethtool eth0 +Settings for eth0: +Supported ports: [ TP ] ##网卡接口支持的类型:FIBRE是光纤,TP是双绞线, +... +Advertised auto-negotiation: Yes +Speed: 1000Mb/s #网卡速率,千兆网卡 +Duplex: Full #全双工 +Port: Twisted Pair +PHYAD: 1 +... +Wake-on: d +Link detected: yes #是否连接到网络,yes是激活状态 +``` + +#### 4. lspci + +`lspci`用于查看pci接口的硬件接入设备信息: + +```shell +#查看网卡设备 +#可以看到broadcom公司的网卡,支持802.11b/g协议 +> lspci | grep Network +06:00.0 Network controller: Broadcom Inc. and subsidiaries BCM4312 802.11b/g LP-PHY (rev 01) +``` + +#### 5. ifconfig + +```shell +#查看当前网络设备状态 +#MTU:1500指物理拆包大小为1500字节 +#RX表示收到报文 +#TX表示发出报文 +#注意字段errors:0 dropped:0,表示当前网卡工作正常,无丢包 +> ifconfig +eth1 Link encap:Ethernet HWaddr FA:16:3E:7D:08:7A + inet addr:10.209.33.130 Bcast:10.209.33.255 Mask:255.255.255.0 + UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 + RX packets:30969152096 errors:0 dropped:0 overruns:0 frame:0 + TX packets:33741508362 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:1000 + RX bytes:7099702494395 (6.4 TiB) TX bytes:5287979481691 (4.8 TiB) +``` + +#### 6. tcpdump + +`tcpdump`常用于抓包分析,过程如下: + +```shell +#使用tcpdump抓包 +#80为nginx对外暴露http服务的端口 +#8878为api-gateway的http监听端口 +> tcpdump -i any port 8878 -nn +... +#nginx向api-gateway发送http请求(正常) +02:04:34.085819 IP localhost.56316 > localhost.8878: Flags [P.], seq 799687:800138, ack 492088, win 1024, options [nop,nop,TS val 2055170395 ecr 2055156376], length + 451 +#api-gateway响应窗口,响应http数据(正常) +02:04:34.087436 IP localhost.8878 > localhost.56316: Flags [P.], seq 492088:492419, ack 800138, win 1024, options [nop,nop,TS val 2055170396 ecr 2055170395], length + 331 +#nginx响应窗口(正常) +02:04:34.087446 IP localhost.56316 > localhost.8878: Flags [.], ack 492419, win 1022, options [nop,nop,TS val 2055170396 ecr 2055170396], length 0 +#间隔了28s15ms, nginx向api-gateway发送http请求 +02:05:02.102019 IP localhost.56316 > localhost.8878: Flags [P.], seq 800138:800589, ack 492419, win 1024, options [nop,nop,TS val 2055198411 ecr 2055170396], length + 451 +#因为api-gateway配置的http超时时间是28s,此时apigateway准备,但尚未向nginx发送FIN包 +#但是nginx也巧合的在刚刚超时的时间点先PUSH了一个包 +#所以api-gateway响应reset包 +02:05:02.102980 IP localhost.8878 > localhost.56316: Flags [R.], seq 492419, ack 800589, win 1024, options [nop,nop,TS val 2055198412 ecr 2055198411], length 0 + ... +``` + +#### 7. ss + +`ss`用于获取当前操作系统socket统计信息。 + +```shell +#查看当前tcp backlog +#当State为LISTEN状态: +# Recv-Q 表示当前等待服务端调用 accept 完成三次握手的 listen backlog 数值; +# Send-Q 表示最大的 listen backlog 数值 +> ss -pl +State Recv-Q Send-Q Local + ... +LISTEN 0 511 *:https +LISTEN 0 511 *:http + ... + +#查看当前socket数量 +# TCP: 7068 表示总共7068个tcp连接 +# estab 65 表示有65个tcp连接建立 +# closed 6974 表示6974个tcp连接处于close状态 +# timewait 6964/0 表示6964个tcp处于timewait, 需要优化 +> ss -s +Total: 241 (kernel 457) +TCP: 7068 (estab 65, closed 6974, orphaned 4, synrecv 0, timewait 6964/0), ports 1333 + +Transport Total IP IPv6 +* 457 - - +RAW 0 0 0 +UDP 8 8 0 +TCP 94 94 0 # 94个tcp包 +INET 102 102 0 +FRAG 0 0 0 +``` + +#### 8.vmstat + +`vmstat`用于对操作系统的虚拟内存、进程、CPU活动进行监控。 + +```shell +#每秒输出1次监控信息,总共输出3次 +# memory 表示内存 +# swap 表示交换空间, si表示每秒从交换区写到内存大小, so每秒写入交换区的内存大小 +# system 表示系统状态,注意in表示每秒中断,cs表示每秒线程上下文切换,超过10w则表示过大 +# linux thread切换耗时约1us +> vmstat 1 3 +procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu----- + r b swpd free buff cache si so bi bo in cs us sy id wa st + 1 0 0 200792 110864 14009092 0 0 0 12 0 0 2 1 97 0 0 + 4 0 0 199844 110864 14009276 0 0 0 32 7275 9111 10 4 80 0 6 + 0 0 0 200332 110864 14009356 0 0 0 104 6990 9649 2 2 94 0 2 +``` + +#### 9.pidstat + +`pidstat` 用于监控全部或指定进程占用系统资源的情况 + +```shell +# -w 表示输出进程的上下文切换情况; +# -u 表示输出进程cpu使用情况 +# -t 表示输出进程中线程统计信息 +# -p 指定进程pid +# 1 5: 每秒输出1次,输出5次 +> pidstat -w -u -t -p 1709 1 5 +Linux 3.10.0-1127.el7.x86_64 (localhost.localdomain) 12/11/2020 _x86_64_ (2 CPU) +#cpu情况 +03:22:57 PM UID TGID TID %usr %system %guest %CPU CPU Command +03:22:57 PM 0 1709 - 0.10 0.12 0.00 0.23 1 ngrok +03:22:57 PM 0 - 1709 0.02 0.01 0.00 0.03 1 |__ngrok +03:22:57 PM 0 - 1711 0.01 0.05 0.00 0.06 1 |__ngrok +03:22:57 PM 0 - 1712 0.02 0.01 0.00 0.03 0 |__ngrok +03:22:57 PM 0 - 1713 0.02 0.01 0.00 0.03 0 |__ngrok +03:22:57 PM 0 - 1714 0.00 0.00 0.00 0.00 1 |__ngrok +03:22:57 PM 0 - 1715 0.02 0.01 0.00 0.03 0 |__ngrok +03:22:57 PM 0 - 1716 0.02 0.01 0.00 0.03 1 |__ngrok +03:22:57 PM 0 - 1731 0.02 0.01 0.00 0.03 1 |__ngrok +#上下文切换情况 +#cswch/s:每秒主动任务上下文切换数量 +#nvcswch/s:每秒被动任务上下文切换数量 +# linux thread切换耗时约1us +03:22:57 PM UID TGID TID cswch/s nvcswch/s Command +03:22:57 PM 0 1709 - 3.73 0.51 ngrok +03:22:57 PM 0 - 1709 3.73 0.51 |__ngrok +03:22:57 PM 0 - 1711 18.83 0.20 |__ngrok +03:22:57 PM 0 - 1712 3.71 0.48 |__ngrok +03:22:57 PM 0 - 1713 3.69 0.50 |__ngrok +03:22:57 PM 0 - 1714 0.00 0.00 |__ngrok +03:22:57 PM 0 - 1715 3.72 0.50 |__ngrok +03:22:57 PM 0 - 1716 3.76 0.49 |__ngrok +03:22:57 PM 0 - 1731 3.68 0.51 |__ngrok +``` + +#### 10.iostat + +`iostat`用于查看磁盘的监控信息: + +```shell +#系统命令iostat +> iostat -x 1 3 #每秒打印1次,打印3次磁盘状态 +#示例第二次状态 +#%util趋于100%,说明磁盘io压力很大 +avg-cpu: %user %nice %system %iowait %steal %idle + 7.98 0.00 3.80 10.33 0.00 77.89 + +Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util +sda 26.00 1480.00 2544.00 1692.00 28800.00 33576.00 29.45 4.18 0.98 1.59 0.06 0.23 98.70 +dm-0 0.00 0.00 316.00 0.00 1384.00 0.00 8.76 0.42 1.36 1.36 0.00 0.88 27.90 +dm-4 0.00 0.00 2198.00 3163.00 27192.00 33576.00 22.67 3.48 0.64 1.48 0.06 0.18 97.50 +``` + +#### 11.strace + +`strace`常用来跟踪进程执行时的系统调用和所接收的信号: + +```shell +#strace分析进程执行命令,30897为nginx的一个worker进程id +# 发现write部分(日志)占用了23%的耗时,此处需要进行优化 +> strace -c -p 30897 #压测30s数据 +% time seconds usecs/call calls errors syscall +------ ----------- ----------- --------- --------- ---------------- + 37.41 0.038220 7 5782 3417 accept4 + 33.51 0.034240 4 9077 epoll_wait + 23.26 0.023760 5 4730 write + 1.99 0.002031 0 4730 close + 1.45 0.001485 0 4730 writev + 1.04 0.001061 0 2365 2365 connect + 0.46 0.000465 0 4730 recvfrom + 0.29 0.000294 0 2365 socket + 0.25 0.000256 0 9078 gettimeofday + 0.25 0.000251 0 7095 epoll_ctl + 0.06 0.000061 0 2365 ioctl + 0.04 0.000045 0 2365 getsockopt +------ ----------- ----------- --------- --------- ---------------- +100.00 0.102169 59412 5782 total +``` + +##### 参考 +- linux thread switching: https://eli.thegreenplace.net/2018/measuring-context-switching-and-memory-overheads-for-linux-threads/ \ No newline at end of file diff --git "a/markdown/2021/20210219-\347\275\221\347\273\234\345\215\217\350\256\256\346\200\273\347\273\223.md" "b/markdown/2021/20210219-\347\275\221\347\273\234\345\215\217\350\256\256\346\200\273\347\273\223.md" new file mode 100644 index 0000000..88d6aaa --- /dev/null +++ "b/markdown/2021/20210219-\347\275\221\347\273\234\345\215\217\350\256\256\346\200\273\347\273\223.md" @@ -0,0 +1,343 @@ +[TOC] + +### 一、网络分层 + +#### 1.1 OSI七层模型 + +由国际标准化组织提出的一种概念模型。该模型将通信系统中的数据流划分为七个层次,每个中间层为其上一层提供功能,其自身功能则由其下一层提供,第1层在底部。 + +#### 1.2 TCP/IP 四层模型 + +OSI七层模型只是一种理论上的概念模型,并没有提供一个可以实现的方法,而只是描述了一些概念,并不一定就完全代表了实际情况。其实在实践中不用分的那么细致,OSI中的一些理论分层可以"合并"实现。实际情况使用的就是TCP/IP四层模型,其定义如下: + +**第四层 应用层:** 相当于组合了OSI七层模型中的会话层、表示层、应用层 + +**第三层 传输层:** 同OSI七层模型中的传输层 + +**第二层 网际层:** 同OSI七层模型中的网络层 + +**第一层 网络访问(链接)层:** 相当于OSI七层模型中的物理层和数据链路层 + +#### 1.3 五层模型 + +除了上面提到的理论上的七层模型和实际使用的四层模型,还有一个五层模型。这个五层模型结合了七层模型和四层模型,主要用于网络协议原理介绍,它与TCP/IP四层模型的区别就是将网络访问(链接)层分为了数据链路层和物理层。OSI七层模型、TCP/IP四层模型和五层模型对比如下: + +
+ +
+ +接下来就以五层协议为标准进行总结。 + +### 二、物理层 + +物理层是模型中最低的一层,它确保原始数据可以在各种物理媒介上传播。所谓的媒介包括光纤、电缆或者电磁波等等,这些媒介可以传输物理信号,比如高电压、低电压。若用1表示高电压,0表示低电压,将数据转化为按照规范组合的0/1序列,就可以在物理媒介上传播了。对于接收设备,也需要接口能接受这些物理信号,将其重新转换为0/1序列,以完成数据的接收。 + +### 三、数据链路层 + +即连接层,在两个网络实体之间提供数据链路连接的创建、维持和释放管理,信息以**帧**为单位传输。所谓帧,就是一段0/1序列组合,其中有收信地址、送信地址、校验序列(探测错误)等等,其中的地址是MAC地址。帧中的数据往往符合更高层协议,供上层使用。数据链路层就像一个送货员,不会关心数据的具体内容。 + +此层中常见的协议有**以太网、WiFi、帧中继**等等,通过这些协议,我们可以建立一个局部网络(局域网),使得一个局域网中的两台计算机可以通信。 + +帧分为**帧头、数据、帧尾**,帧头和帧尾包含同步信息、收发地址信息、差错控制信息等,数据部分则包含网络层传下来的数据,比如IP数据包。 + +帧头包含一段被称为**序言**的0/1序列,开始会以一定的频率发送序言,接收设备要按照一定的频率接收才能做到不丢失信息。就像我们收听录音机之前,需要先调整接收频率,直到听清楚播报内容,这个过程叫做**调频**。在帧中,序言就是为了发送设备和接收设备频率一致,这个过程叫做**时钟复原**。时钟调整好之后,等待帧的**起始信号**(SFD,start frame delimiter,为固定值)。起始信号之后,还有帧的发送地址和目的地址,当然,这个地址是MAC地址。 + +关于数据部分,前面已经提过了,一般包含符合更高层协议的数据,但是由于数据有最小长度限制,所以尾部可能包含一部分**0填充**。 + +帧尾包含**校验序列**(FCS ,Frame Check Sequence),主要是为了校验数据是否发生错误。检测使用**CRC**(Cyclic Redundancy Check,循环冗余检查)算法:在一个n位二进制序列之后增加一个m位的二进制序列,生成长度为n+m的新二进制序列。这个m位的二进制序列可以称之为校验位,m位的校验位与n位的原数据之间存在一定的关联,如果原数据异常,就能通过校验位检测出来。 + +#### 3.1 集线器和交换器 + +一台集线器或者交换器上有很多端口,每个端口都能连接一台设备。 + +**集线器:**一台设备将帧发送到集线器,集线器会将帧广播到其他所有端口,每台设备收到帧之后,检查帧的目标地址是不是本设备的MAC地址,如果不是则忽略该帧。也就是以**广播**的方式转送消息,所有设备都能收到其他设备发送的消息。存在的问题一个是**安全性**,当然可以加密消息;另外一个是**不允许多路同时通信**,如果两台设备同时向集线器发送消息,那么集线器会提醒发生冲突,设备上可以实现冲突检测算法,如果发生冲突,那么随机等待一段时间再发送。WiFi的工作方式与此类似,所以WiFi需要特别注重加密(WPA、WPA2)。 + +**交换器:** 交换器记录了每个设备的MAC地址,当交换器收到帧之后,会根据帧的目的地址和本机存储的MAC地址记录,只将帧发送到对应的端口,进而准确发送到对应的设备。交换器允许多路同时通信。 + +### 四、网络层 + +数据链路层使得同一个局域网中的的计算机可以相互通信,但是也只限于同一个局域网。要想让一个局域网里的计算机和以太网上的另一台计算机通信,则需要一些其它手段。这就是网络层需要做的事儿:提供**路由**和**寻址**的功能,使两终端系统能够互连且决定最佳路径,并具有一定的**拥塞控制**和**流量控制**的能力。 + +在数据链路层的帧中,只有送信地址和收信地址(MAC地址),但是跨局域网通信至少需要四个地址(比如数据从一个计算机经过WiFi接口,以太网接口,再到另一个计算机),又由于数据链路层已经存在并且正在使用,不能随意对其更改,所以只能在数据内容上做文章:将目标地址、校验信息等写到数据内容开头,反正数据链路层也不关心具体的数据,这就是IP协议。网络层协议由IP协议规定和实现,所以网络层又称为**IP层**。 + +关于目标地址,IP协议需要的地址和数据链路层需要的地址不一样。数据链路层的地址是物理地址(MAC地址),相对应的,网络层使用的地址叫做IP地址。当设备连接网络,设备将被分配一个IP地址用作标识。通过IP地址,不同网络间的设备可以互相通讯,如果没有IP地址,我们将无法知道哪个设备是发送方,哪个是接收方。 + +大多数接入Internet的方式是把主机通过局域网组织在一起,然后再通过交换机或路由器等设备和 Internet 相连接。在同一个局域网络中的计算机不需要网络层,仅依靠数据链路层就可以通信,但是对于不同的网络之间相互通信则必须借助路由器等设备。 + +#### 4.1 MAC地址和IP地址 + +两台设备进行通讯的前提就是知道双方的地址,而IP地址和MAC地址都是地址,那么仅仅依靠IP地址或者MAC地址能否完成路由呢? + +随着网络接入设备越来越多,为了解决寻址的问题,网络结构被层次化构造,被划分为了很多个子网,在路由的时候,如果数据包的目的地在其他子网,那么只需要把数据包路由到该子网,剩下的工作交由子网内部处理,这极大的减少了路由器计算量。 + +MAC地址有**48位**,可以唯一表示一个网卡,是网络设备制造商生产时写在硬件内部的。而基于子网的这个结构,如果使用MAC地址进行以太网设备的寻址,那么路由器需要记住每个MAC地址所在的子网。但是理论上,MAC地址可以有2^48个,这个数据量需要占用的内存就不允许我们这样做,所以有了IP地址。(**以下对于IP地址的描述都基于IPV4**)。 + +IP地址由IP协议提供统一的地址格式。一个IP地址包含两个标识码:**网络ID**和**主机ID**。同一个局域网络上的所有设备都是用同一个网络ID,网络上的一个主机(包括路由器)有一个主机ID与其对应。IP地址(IPV4)是一个**32位**的二进制数,通常被分割为4个8位二进制数,一共4个字节,比如通过**点分十进制**表示格式为:123.224.213.23,其中每个部分都是对应8位二进制数的十进制形式。同时,Internet委员会定义了5种IP地址类型以适合不同容量的网络,即A类~E类。以C类IP地址为例: + +一个C类IP地址的4段号码中,前三段为网络ID,第四段为主机ID,也就是说C类IP地址由3字节(24位)的网络地址和1字节(8位)的主机地址组成。而C类地址的最高位固定为110(二进制),所以其能表示的地址范围为:11000000 00000000 00000000 00000001 - 11011111 11111111 11111111 11111110,对应的点分十进制即:192.0.0.1-223.255.255.254,子网掩码为255.255.255.0,每个网络支持的最大主机数为256-2(除去私有地址192.168.0.0到192.168.255.255)=254台。 + +有了IP地址,再回到寻址的问题。IP地址是和子网相关的,对于通一个子网上的设备,其IP地址前缀都是一样的(网络地址),路由器能直接通过地址前缀定位到子网,而不用像使用MAC地址那样无助。由于IP地址必须得等到设备(网卡)接入一个子网之后才能进行分配,在设备还没有IP地址的时候还是需要使用MAC地址来区分设备。一个设备的IP地址是可以变更的,当IP地址变了之后,还怎么唯一确定一个设备呢?所以IP地址切合的是层次结构,不能唯一标识一个设备。一个形象的比喻就是,MAC地址就像自己的ID号,能唯一标识自己,而IP地址就像一个邮编,方便别人找到自己的所在地,两者缺一不可。 + +#### 4.2 IPV6 + +由于IPv4协议的地址为32位,所以它可以提供2^32,也就是大约40亿个地址,如果地球人每人一个IP地址的话,IPv4地址已经远远不够,更何况人均持有的入网设备可能要远多于一个,所以IPV4地址迟早会耗尽。IPV6就是在IPV4的基础上做了加长改进,IPV6地址为**128位**,解决了IPV4地址耗尽的问题。但是由于老的路由器只支持IPV4的IP包,但是无法理解IPV6格式的IP包,所以IPV4到IPV6的迁移需要伴随着路由设备的更新,而且IPV4网络早已广泛使用,所以IPV4到IPV6的迁移是一个较为漫长的过程。 + +#### 4.3 IP包与接力 + +数据链路层以帧为单位传输,而在网络层,无论是UDP还是TCP,都是用IP数据包传递信息。IP数据包分为头部和数据,头部是为了能够实现IP协议而附加的信息,数据部分则是真正要传输的数据。 + +IP包的传输要通过路由器的接力,每个主机和路由中都存在一个路由表,可以根据路由表找到IP数据包传送需要走的路线。就像坐大巴,我们从A城市出发前往D城市,那么可能需要在A城市购票前往B城市,在B城市购票前往C城市,然后再从C城市到达D城市。这个就是路由的过程,只是由于网络被设计为分层的结构,以此基础进行路由效率较高。 + +路由表包含地址、网关、子网掩码和网卡等等信息,表里描述的是一种规则:如果目的地址符合a规则,那么路由器将IP包发送到a规则规定的中间路由器,该中间路由器收到数据后,会继续根据目的地址和本机路由表重复上述路由规则。如果根据规则,IP包能够从本机网卡发送出去,那么就不用继续路由,直接由本机网卡发送,这个时候IP包已经到目标局域网了。需要注意的是,一个路由器可以拥有多个网卡,也就是可以同时接入多个局域网。就这样,数据从主机出发,根据路由表不断接力,最终达到目标地址所在局域网,然后再由数据链路层根据帧中的MAC地址将数据发送给指定主机。 + +#### 4.4 ARP协议(Address Resolution Protocol) + +IP包正常接力的前提是,对于一个局域网内,每一台主机和路由器都能知道局域网内主机的IP地址和其MAC地址的对应关系,每一台主机或路由中都有一个ARP Cache用以存储局域网内主机IP地址和MAC地址的对应关系,有了这个对应关系,就能通过一个IP地址获取对应的MAC地址。而这个关系通过ARP协议传播到局域网的每个主机和路由:发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。 + +笼统的说,IP地址处于网络层,MAC地址处于数据链路层,相互没有直接关联,通过以太网发送IP数据包时,需要先封装IP地址(网络层),再封装MAC地址(数据链路层)。但由于发送时只知道目标IP地址,不知道其MAC地址,又不能跨网络层和数据链路层,所以需要使用ARP协议。使用ARP协议,可根据网络层IP数据包包头中的IP地址信息解析出目标硬件地址(MAC地址)信息,以保证通信的顺利进行。ARP包需要包裹在一个帧中,它处于**数据链路层和网络层之间**。IPV6中使用的是**NDP协议**(Neighbor Discovery Protocol),工作在网络层。 + +##### 4.4.1 ARP欺骗与防治 + +ARP协议是建立在网络中各个主机互相信任的基础上的,局域网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP缓存。由此攻击者就可以向某一主机发送伪ARP应答报文,使发送的信息无法到达预期的主机或到达错误的主机,这就构成了一个ARP欺骗。不过ARP欺骗也可以当做正当用途,比如将未登录用户的请求强制转向登录界面。 + +**ARP欺骗防治手段** + +**1. ARP静态配置**:由于需要静态配置每一台计算机的ARP表,所以不适用于大型网络。 + +**2. DHCP Snooping**:启 DHCP Snooping 功能后,网络中的客户端只有从管理员指定的DHCP服务器获取IP地址,必须将交换机上的端口设置为信任(Trust)和非信任(Untrust)状态,交换机只转发信任端口的 DHCP OFFER/ACK/NAK报文,丢弃非信任端口的 DHCP OFFER/ACK/NAK 报文,从而达到阻断非法DHCP服务器的目的。 + +#### 4.5 RIP协议(Routing Information Protocol) + +IP包正常接力的另一个前提是每个主机和路由器上都有正确且合理的路由表。这个路由表描述了网络的拓扑结构,如果你了解自己的网络连接,可以手写自己主机的路由表,但是一个路由器可能有多个出口,路由表可能会很长,并且周围连接的其他路由器可能会随时发生变动(比如新增路由器或者路由器坏掉),我们就需要路由表能及时将转向导向其他的路由出口,所以需要一种能**自动探测网络生成路由表**的协议。 + +RIP协议就是这样一种协议,它是一种**动态路由选择协议,通过距离来决定路由表**。RIP协议基于**距离矢量算法**(DistanceVectorAlgorithms),使用“跳数”(即metric)来衡量到达目标地址的路由距离。这种协议的路由器只关心自己周围的世界,只与自己相邻的路由器交换信息,范围限制在15跳(15度)之内,再远它就不关心了。路由器向周围的路由器和主机广播自己前往各个IP的距离,收到RIP数据包的路由器和主机根据RIP包和自己到发送RIP包的主机的距离,算出自己前往各个IP的距离(比如我收到A路由器到IP1的距离为3,而我到A路由器的距离为1,那么我经过A路由到IP1的距离就为3+1=4)。如果计算出来的新距离优于路由表缓存,那么更新路由表,否则保持不变,就这样在各个点不断重复RIP广播/计算距离/更新路由表的过程,最终所有的主机和路由器都能生成最合理的路径。 + +由于RIP的跳数标准(超过15跳认为不可达),所以RIP更多应用于互联网的一部分。这样一个互联网的部分往往属于同一个ISP或者有同一个管理机构,所以叫做**自治系统**(AS,autonomous system)。自治系统内部的主机和路由根据通向外部的**边界路由器**来和其它的自治系统通信。各个边界路由器之间通过 **BGP** (Border Gateway Protocol,**边界网关协议**)来生成自己前往其它自治系统的路由表,而自治系统内部则参照边界路由器,使用RIP来决定路由表。BGP的基本工作过程与RIP类似,但在考虑距离的同时,也权衡比如政策、连接性能等其他因素,再决定路由表。 + +#### 4.6 IP协议 + +IP协议认为自己所处的环境是不可靠的,比如路由器坏掉、电缆断裂等,所以IP数据包在传输过程中如果出现错误,那么IP数据包会直接被丢弃,没有重试之类的补救措施,IP协议保持较为简单的处理流程,更加复杂的数据可靠性控制较由高层协议处理。 + +前面提到的IP接力是根据路由表抉择路径的,但是在往同一个目的地发送连续的IP数据包的过程中,路由表可能会更新,比如出现了一条新的捷径,那么后发送的IP数据包由于走了新的捷径,就可能比之前发送的IP包先到达目的地,这样就可能导致IP包到达的顺序和发送的顺序不一致。即使IPv6中的**Flow Label**可以建议路由器将一些IP包保持一样的接力路径,但这只是“建议”,路由器可能会忽略该建议。 + +#### 4.7 ICMP协议(Internet Control Message Protocol) + +ICMP协议处于网络层和传输层之间,用于在IP主机、路由器之间传递控制消息。它基于IP协议,一个ICMP包封装在IP数据包中。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。ICMP协议传输的信息可以分为两类: + +一类是**错误信息**:上面提到的IP协议本身不会保证IP数据包的可靠性,如果IP数据包丢失,那么IP协议不会做什么补救措施,所以上游发送IP数据包的主机或路由器不知道下游数据异常,会继续发送数据包,最终导致数据包丢失。而通过ICMP包,下游的路由器和主机可以将错误信息上报给上游,从而让上游路由器和主机能够做些补救措施,但是ICMP只负责上报信息,后续的处理它并不负责。 + +一类是**咨询类信息**:比如某台设备询问路径上的每个路由器都是谁,然后各个路由器同样用ICMP包回答。 + +有了ICMP协议,人们可以更加方便的对IP协议出现的数据问题进行**排查和错误纠正**,**ping**和**traceroute**这类网络诊断工具就是基于ICMP协议。但是也有骇客利用伪造的IP包引发大量的ICMP错误消息恢复,并将这些 ICMP包导向受害主机,从而形成DOS攻击。 + +### 五、传输层 + +有了上面物理层、数据链路层和网络层协议,两个以太网中的计算机已经可以实现通信了,但是一个计算机中有许多的进程,每个进程都可能有通信的需求。对于每一个进程,我们需要保证数据包准确的送到该进程之中。参照网络层扩展数据链路层的背景,我们也只能在内容数据上添加更加详细的地址:**端口**。传输层协议(比如TCP、UDP)使用端口号来识别进程,为应用进程提供**端到端**的通信服务。网络层只是根据IP地址将源结点发出的数据包传送到目的结点,而传输层则负责将数据可靠地传送到相应的端口。 + +#### 5.1 UDP协议(User Datagram Protocol) + +UDP协议的传输和IP协议类似,也是不可靠的,可以将UDP协议看作IP协议暴露在传输层的一个接口,其他没有什么大的区别。之所以设计UDP协议的一个重要原因是,IP协议没有端口的概念,它只是负责IP到IP的传输,但是不同计算机进程之间的通信要依赖端口,也就是传输层协议做的事,UDP协议是一个传输层协议,实现了端口,从而让数据包可以在IP协议的基础上进一步发送到某一个端口。特别是对于不要求可靠性的传输。 + +#### 5.2 TCP协议(Transmission Control Protocol) + +TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。流的特点是顺序,TCP协议是基于IP协议的,所以数据最终还是以IP数据包为单位传输。如果需要传输的数据很长,那么不能将整个数据放到一个IP数据包中,所以TCP协议封装到IP数据包的不是整个数据,而是TCP协议规定的**分段**(segment)或者叫做**片段**。分段结构类似于IP数据包,包含**头部**和**数据部分**,其中头部包含该片段的**序号**,依赖该序号,接收方就能做到按照流的顺序接收数据。比如已经接收到了分段1、分段2、分段3,那么接收方就开始期待片段4。如果接收到不符合顺序的数据包(比如分段8),则接收方的TCP模块可以拒绝接收,以达到按顺序接收的目的。一个分段在网络中有**最大存活时间**,称之为**MSL**(Maximum Segment Lifetime)。 + +##### 5.2.1 可靠性保障-ACK和重复发送 + +由于片段在传输过程中可能会丢失,所以接收主机收到的数据可能是残缺不堪的。TCP的做法是在每收到一个正确的、符合次序的片段之后,就向发送方发送一个特殊的TCP片段作为**ACK**回执,告知发送方已经接收到该消息。如果一个片段序号为M,那么对应的ACK为M+1,也就是接收方期待接收的下一个片段的序号。如果发送方在一定时间等待之后,还是没有收到ACK回复,那么它就认为之前发送的片段发生了异常。发送方会**重复发送**那个出现异常的片段,等待ACK回复,如果还是没有收到,那么再重复发送原片段... 直到收到该片段对应的ACK回复。 + +##### 5.2.2 滑窗管理 + +按照上面ACK和重复发送的逻辑:发送->等待ACK->发送->等待ACK.......。在等待ACK的时间,网络都处于闲置状态,最好能利用好这个时间。为了解决这个问题,发送方和接收方各有一个滑窗,当片段位于滑窗中时,表示TCP正在处理该片段。滑窗中可以有多个片段,也就是可以同时处理多个片段,滑窗越大,越大的滑窗同时处理的片段数目越多。TCP协议也有**实时调整滑窗大小**的算法,以提高效率。 + +我们假设一个可以容纳三个片段的滑窗,片段从左向右排列。对于发送方来说,滑窗的**左侧**为已发送并收到ACK的片段序列,滑窗**右侧**是尚未发送的片段序列。滑窗中的片段(比如片段5,6,7)被发送出去,并等待相应的ACK。如果收到片段5的ACK,滑窗将向右移动。这样新的片段从右侧进入滑窗内,被发送出去,并进入等待状态。在接收到片段5的ACK之前,滑窗不会移动,即使已经收到了片段6和7的ACK。这样,就保证了滑窗左侧的序列是已经发送的、接收到ACK的、符合顺序的片段序列。 + +对于接收方来说,滑窗的**左侧**是已经正确收到并且回复过ACK的片段(比如片段1,2,3,4),**滑窗中**是期望接收的片段(比如片段5, 6, 7)。同样,如果片段6,7先到达,那么滑窗不会移动。如果片段5先到达,那么滑窗会向右移动,以等待接收新的片段,如果出现滑窗之外的片段,比如片段9,那么滑窗将拒绝接收。 + +对于ACK,如果每个TCP分段都回复一个ACK,那么整个网络流量无疑就翻倍了。事实上,可以将ACK和其他数据片段一起发送,并且不用对每个TCP分段都发送ACK,而是通过一个ACK来通知多个分段接收成功,也就是**ACK合并**。接收方在接收到片段,并应该回复ACK的时候,会故意延迟一些时间,如果在延迟的时间里有后续的片段到达,就可以利用累计ACK来一起回复。比如,滑窗还没接收到片段7时,已经接收到片段8,9。当滑窗最终接收到片段7时,滑窗送出一个回复号为10的ACK回复。发送方收到该回复后会了解到片段10之前的片段已经被成功接收。这个过程就不用发送片段7,8所需的两个ACK。 + +通过调整滑窗大小,可以方便的**控制TCP流量**。TCP头部中有部分**window size区域**,通过这个区域,接收方将window大小通知给发送方,从而指导发送方修改窗口大小,发送方在收到通知后,会调整自己滑窗的大小,如果发送窗口变小,数据发送速率就会降低。 + +如果接收方不能处理消息,可以通知发送方,指导其将窗口大小变更为0(**零窗口**),让其停止发送。当接收方经过一段时间的处理,能继续处理消息时,可以以ACK的方式通知发送方,让发送方调整其窗口大小,恢复发送。但是ACK可能会丢失,如果ACK丢失,那么TCP传输就陷入了**死锁状态(**发送方一直处于零窗口**)**。为了防止死锁的出现,发送方会在零窗口后,向接收方发送1byte的TCP分段,并等待包含窗口大小的ACK回复,以此当做**探测**的手段。如果探测结果显示窗口依然为0,发送方会等待更长的时间,然后再次进行窗口探测,直到TCP传输恢复。同时,接收方宣告的窗口必须达到**一定的尺寸**,否则等待,这是为了防止窗口太小而导致TCP通信分段中包含的数据很小,浪费流量(主要是TCP分段头部),除非需要最小化延迟的TCP应用(比如命令行互动)。 + +##### 5.2.3 三次握手 + +它使用三次握手建立**全双工连接**,流程如下: + +客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。 + +服务器端收到SYN报文,回应一个SYN (SEQ=y)+ ACK(ACK=x+1)报文,进入SYN_RECV状态。 + +客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established(连接已建立)状态。(图片来自网络) + +
+ +
+ + + +##### 5.2.4 四次挥手 + +TCP使用四次挥手终止连接。由于TCP连接属于全双工,在一次连接中双方都可以发送数据,所以两端都需要单独进行关闭。客户端主动断开连接的流程如下: + +一个TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送。(主动关闭) + +服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。(服务端执行被动关闭) + +服务器关闭客户端的连接,发送一个FIN给客户端。 + +客户端发回ACK报文确认,并将确认序号设置为收到序号加1。(图片来自于网络) + +
+ +
+ + + +TCP连接断开的原则是当一方完成它的数据发送任务后发送一个**FIN**来终止这个方向的连接,首先进行关闭的一方将执行**主动关闭**,而另一方执行**被动关闭**。收到一个 FIN只意味着这一方向上没有数据流动,但是一个TCP连接在收到一个FIN后仍能发送数据(另一端)。这主要发生在第二和第三阶段之间:客户端(主动方)到服务器(被动方)的连接断开,但是服务器到客户端的连接还未断开。此时服务端可以继续向客户端发送数据,但是客户端不能向服务器发送数据。 + +第四步客户端发送的ACK可能会丢失,导致服务器无法收到,服务器如果没有收到ACK,将会不断重复发送FIN片段,直到收到客户端发送的ACK。所以客户端不能立即关闭,客户端会在ACK发出之后进入到一个**TIME_WAIT**状态,客户端会设置一个计时器,等待**2MSL**。如果在该时间内再次收到FIN,那么客户端会重发ACK并再次等待2MSL。MSL在前面做过介绍,代表TCP分段的最大存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL的等待时间,客户端都没有再次收到FIN,那么客户端就推断ACK已经被成功接收,则结束TCP连接。 + +注:以上的描述都是客户端作为断开连接的主动方,事实上服务器也可以作为关闭的主动方 + +##### 5.2.5 堵塞控制 + +随着网络设备接入越来越多,网络开始变的繁忙,网络中出现了大量的堵塞。类似于上下班的堵车,IP数据包在网络中经过一个个路由的接力,如果一个路由器过度繁忙,会丢弃一些数据包。UDP协议还好,不用保证数据的可靠性,但是TCP协议需要保证数据的可靠传输。如果包含TCP片段的IP数据包丢失时,TCP协议会重复发送该片段,于是网络中产生了更多的数据包,原本繁忙的状况得到加剧,构成恶性循环,这种情况被称作**堵塞崩溃**。为了解决这一问题,TCP协议加入了堵塞控制。 + +TCP协议会探测网络状况,如果网络发生了拥堵,那么会控制自己发送片段的速率,以缓解网络状况。至于探测的方法主要有**ACK超时**和**重复ACK**,一旦发送方认为TCP片段丢失,则认为网络中出现堵塞。 + +#### 5.3 TCP协议和UDP协议 + +UDP协议的产生要晚于TCP协议。早期的网络开发者开发出IP协议和TCP协议分别位于网络层和传输层,所有的通信都要先经过TCP封装,再经过IP封装(应用层->TCP->IP)。开发者将TCP/IP视为相互合作的套装。但很快网络开发者发现,IP协议的功能和TCP协议的功能是相互独立的。对于一些简单的通信,我们并不需要可靠的传输,也就不需要TCP协议复杂的建立连接和断开连接的方式(特别是在早期网络环境中,如果过多的建立TCP连接,会造成很大的网络负担)。UDP协议随之被开发出来,作为IP协议在传输层的"傀儡"。这样,网络通信可以通过应用层->UDP->IP的封装方式,绕过TCP协议。由于UDP协议本身异常简单,实际上只为IP传输起到了桥梁的作用,而UDP协议也可以相对快速地处理这些简单的通信。 + +### 六、应用层 + +就像现实生活中,不同国家的人可能使用不同的语言,要与一个国家的人交流,需要用他使用的语言,不然就会存在沟通问题。对应到应用程序,一个计算机中不同的进程所做的事可能不一样,而他们所能理解的数据规则也可能不一样,除了让进程能够准确接收数据包外(**传输层保证**),还需要保证它能够正确理解数据包。应用层协议就是做这样一件事:**进一步规范数据包**,以达到我们的目的。 + +#### 6.1 DNS协议 + +域名是IP地址的代号,通常由字符构成,比如blog.csdn.net,这要比101.200.35.175这种IP格式更加容易记忆。域名解析系统(DNS)就负责将域名翻译为IP地址,域名和IP地址的对应关系存储在DNS服务器,这些服务器是指在网络中进行域名解析的一些服务器(计算机),它们也有自己的IP地址,使用DNS协议进行通信。DNS协议**基于UDP协议**,属于应用层协议。 + +
+ +
+ + + +DNS服务器构成一个**分级的树状体系**。上图中(网图),每个节点为一个DNS服务器,每个节点都有自己的IP地址。树的顶端为用户电脑出口处的DNS服务器,树的末端是真正的域名/IP对应关系记录。一次DNS查询就是从树的顶端节点出发,最终找到相应末端记录的过程。在Linux下,可以使用**cat /etc/resolv.conf**,在Windows下,可以使用**ipcofig /all**,来查询出口DNS服务器。 + +中间节点根据域名的构成,将DNS查询引导向下一级的服务器。比如说一个域名cs.berkeley.edu,DNS解析会将域名分割为cs, berkeley, edu,然后按照相反的顺序查询(edu, berkeley, cs)。出口DNS首先根据edu,将查询指向下一层的edu节点。然后edu节点根据berkeley,将查询指向下一层的berkeley节点,这台berkeley服务器上存储有cs.berkeley.edu的IP地址。 + +并不是每次域名解析都要完整的经历解析过程,本机DNS Resolver通常都有DNS缓存,在进行DNS查询之前,计算机会先查询缓存。 + +##### 6.1.1 反向DNS + +上面的DNS查询均为正向DNS查询:已经知道域名,想要查询对应IP。而反向DNS(reverse DNS)是已经知道IP的前提下,想要查询域名。 + +#### 6.2 HTTP协议 + +即超文本传输协议,是一个简单的请求(request)-响应(response)协议,它通常运行在TCP协议之上,属于应用层协议。它指定了客户端可能发送给服务器什么样的消息,以及得到什么样的响应。 + +HTTP协议规定了请求和响应的格式,请求包含**请求行、请求头、空行和请求体,**响应包含**状态行、响应头、空行和响应体**。 + +##### 6.2.1 请求行、请求头和请求体 + +**请求行:** + +只有一行,包含三部分信息,分别是**请求的方法**(比如POST、GET等),**请求的资源路径**和**HTTP协议的版本**。比如 GET /shakespeare/v2/notes/636c1ee16eba/book HTTP/1.1。 + +HTTP/1.1协议定义的8中请求方法,但是并不是所有的服务器都必须支持所有的请求方法,比如APACHE作为一个网页服务器,支持GET、POST等方法,但是不必支持CONNECT等方法: + +**GET**:主要用于从服务器获取资源,也可以用于传输不重要的数据,通过改写URL的方式实现:URL?param={param}&... + +**POST**:主要用于向服务器提交数据,数据位于http请求的主体,比如提交表单。 + +**HEAD**:类似于GET,不过返回的响应中没有数据,响应头中包含的元信息应该和一个GET请求的响应消息相同,常用来测试超链接的有效性、可用性和是否被串改等。 + +**DELETE**:请求服务器删除Request-URL所标识的资源。 + +**CONNECT**:HTTP/1.1协议预留的方法,能够将连接改为管道方式的代理服务器。通常用于代理服务器,以代理服务器为跳板代替用户发起请求,然后将数据原模原样的返回。 + +**OPTIONS**:类似于HEAD方法,这个方法会请求服务器返回该资源所支持的所有HTTP请求方法,该方法会用*来代替资源名称,向服务器发送OPTIONS请求,可以测试服务器功能是否正常。 + +**TRACE**:请求服务器回显其收到的请求信息,该方法主要用于HTTP请求的测试或诊断。 + +**PUT**:从语义上来说,PUT方法向指定资源位置(URI)上传其最新内容,如果已经存在则完全覆盖,否则新建。PUT和POST的主要区别可以从**幂等性**切入,由于PUT是上传最新的内容,服务器原有资源会被完全代替,所以是幂等的。如果只提交部分内容,又需要幂等,那么不要使用PUT(因为会完全覆盖)。而POST在服务端的实现是不可预知的,如果每次更新部分内容,那么不能保证幂等性。 + +**请求头:** 头信息可以有多行,每一行都是一个键值对,关键字和值用英文冒号“:”分隔。请求头通知服务器有关于客户端请求的信息比如 Content-type: text/plain。头信息是对请求行的补充。 + +**请求体:** 主体部分包含了具体的资源 + +##### 6.2.2 状态行、响应头和响应体 + +**状态行**: + +类似于请求行,响应行也包含三部分信息,分别是**协议版本、状态码和状态描述**。比如 HTTP/1.1 200 OK,其中OK是对状态码200的描述,只是为了方便人类阅读,计算机只关心状态码,即这里的200,表示一切OK,资源正常返回。状态码有很多,比如302:重定向,404:找不到资源,403:权限不足等等 + +**响应头**:类似请求头,Content-type说明了主体所包含的资源的类型。根据类型的不同,客户端可以启动不同的处理程序,比如: + +text/plain:普通文本 + +text/html:HTML文本 + +application/json; charset=utf-8:json数据,编码为utf-8 + +**响应体**:返回的资源,类型参照Content-type。 + +##### 6.2.3 HTTP请求流程 + +通过浏览器访问一个网页: + +1、浏览器向DNS服务器请求解析该 URL 中的域名所对应的 IP 地址 + +2、解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立 TCP 连接 + +3、浏览器发起HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器 + +4、服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器 + +5、释放 TCP 连接:若connection 模式为close,则服务器主动关闭TCP 连接,客户端被动关闭连接,释放TCP 连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求 + +6、浏览器将该 html 文本并显示内容 + +##### 6.2.4 HTTP/2改进 + +**多路复用**(**Multiplexing**) + +在 HTTP/1.1 协议中 ,浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制,超过限制数目的请求会被阻塞。所以一些站点会有多个CDN域名,目的就是为了变相解决对同一域名的请求限制问题。多路复用则允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。所以HTTP/2可以不依赖建立多个TCP连接实现多流并行处理。HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的**帧,**这些帧对应着逻辑流中的消息,并行的在同一个TCP连接上流转。 + +**二进制分帧** + +在HTTP/2(应用层)和传输层之间新加入了一个二进制分帧层。在二进制分帧层中,HTTP/2 把传输的消息分割为更小的消息和帧,并且采用二进制的格式和编码,而非HTTP/1.0的文本格式,效率更高。其中 HTTP1.x 的头部信息会被封装到 HEADER frame,而相应的消息体则封装到 DATA frame 里面。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。 + +**服务器推送** + +服务端可以在发送HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求。服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,服务器不会随便推送第三方资源给客户端。 + +**头部压缩** + +HTTP/2对消息头采用HPACK(专为HTTP/2头部设计的压缩格式)进行压缩传输,能够节省消息头占用的网络流量。而HTTP/1.x每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。 + +#### 6.3 DHCP协议(Dynamic Host Configuration Protocol,动态主机配置协议) + +DHCP协议**基于UDP协议**,常被应用在大型的局域网络环境中,主要作用是集中的管理、分配IP地址,使网络环境中的主机动态的获得IP地址、Gateway地址、DNS服务器地址等信息,并能够提升地址的使用率。网络上的点对点沟通需要有IP地址,但是新接入网络中的主机需要通过DHCP获取IP地址,所以DHCP需要依赖UDP协议的广播通信,把UDP数据包发送到网络的广播地址上,网络上的每个设备都能收到。 + +DHCP通信分为四步: + +Discovery:客户机发广播,搜寻DHCP服务器 + +Offer:DHCP服务器发出邀请,提供一个可用的IP地址 + +Request:客户机正式请求使用该IP地址 + +Acknowledge:DHCP服务器确认,并提供其他配置参数 + +##### 6.3.1 DHCP攻击 + +针对DHCP的一种攻击办法是从服务器那里骗IP地址。攻击者的电脑可以不断发出DHCP请求,冒充成新入网的客户机。于是DHCP服务器的地址池被耗干,无法分配地址给后来的用户。后来的用户再也没法使用网络服务。攻击者很可能会继续下套,攻击者占有了大量IP地址,可以装扮成新的DHCP服务器,把自己骗来的IP地址分配给网络上的新用户。攻击者伪装DHCP服务器之后,可以让自己成为DNS服务器或者网络的出口。于是客户机的域名解析和外网通信,必须经过攻击者的电脑。这个时候,攻击者可以偷听通信、伪装成客户机、假扮成某个域名的网站。 + + + +#### 参考 + +- 网络协议极简总结: https://iflow.uc.cn/webview/news?app=uc-iflow&aid=11284875513098967822&cid=100&zzd_from=uc-iflow&uc_param_str=dndsfrvesvntnwpfgicp&recoid=&rd_type=share&sp_gz=0&pagetype=share&btifl=100&uc_share_depth=1 \ No newline at end of file diff --git "a/markdown/2021/20210427-grpc\347\263\273\345\210\227-01proto\345\215\217\350\256\256\344\273\243\347\240\201\347\224\237\346\210\220.md" "b/markdown/2021/20210427-grpc\347\263\273\345\210\227-01proto\345\215\217\350\256\256\344\273\243\347\240\201\347\224\237\346\210\220.md" new file mode 100644 index 0000000..d0f5856 --- /dev/null +++ "b/markdown/2021/20210427-grpc\347\263\273\345\210\227-01proto\345\215\217\350\256\256\344\273\243\347\240\201\347\224\237\346\210\220.md" @@ -0,0 +1,111 @@ +### + +[TOC] + +### grpc系列-01: proto协议代码生成 + +#### 1. proto文件定义如下 + +base.proto + +```protobuf +//base.proto +syntax = "proto3"; + +package base; //指定包名,如果下面option go_package指定,则被替换 +option go_package = "base"; //会在--go-out指定目录下,生成base目录 + +message UserID { + string Tel = 1; //手机号 + string Email = 2; //邮箱 +} + +message CommonResp { + int32 Code = 1; //响应code + string Message = 2; //消息 +} +``` + +demo.proto + +```protobuf +//demo.proto +syntax = "proto3"; + +package demo; //指定包名,如果下面option go_package指定,则被替换 +option go_package = "protocol/pb/demo1"; //会在--go-out指定目录下,生成protocol/pb/demo1目录 + +import "base/base.proto"; //需要用到-I 或者 --proto_path 来指定引包路径 + +message AddUserReq { + base.UserID UserId = 1; //用户id +} + +message AddUserResp { + base.CommonResp Common = 1; +} + + +service DemoService { //在 --go-out=plugins=grpc 会生成服务的client及server + //添加用户 + rpc AddUser (AddUserReq) returns (AddUserResp) { + } +} +``` + +#### 2. proto文件目录结构 + +```sh +└── proto + ├── base + │   ├── base.pb.go + │   └── base.proto #源文件base.proto + ├── demo + │   ├── demo.pb.go + │   └── protocol + │   └── pb + │   └── demo1 + │   └── demo.pb.go + └── demo.proto # 源文件demo.proto +``` + +#### 3. 生成protobuf的go代码 + +```bash +#需要下载两个bin文件 +#protoc: https://github.com/protocolbuffers/protobuf/releases +#protoc-gen-go: +#(1)go get -v github.com/golang/protobuf/protoc-gen-go +#(2)go install github.com/golang/protobuf/protoc-gen-go + +#进入proto目录 +> cd ./proto + +#生成base.proto的go代码文件 +# --go_out指输出go代码文件,由于指定为当前路径,则最终会在当前路径创建base文件, +# 并生成 base.pb.go +# pb代码文件路径为: ./base/base.pb.go +> protoc --go_out=./ ./base/base.proto + +#生成demo.proto 示例1 +# -I或者--proto_path,指引包路径,因为会引base/base.proto, 所以要指定当前路径 +# --go_out=plugins=grpc 说明要同时生成grpc的client及服务接口,生成路径为./demo下 +# 由于demo.proto下定义了go_package。则会在./demo下,生成目录protocol/pb/demo1 +# 并且最后包名为demo1 +# pb代码文件路径为: ./demo/protocol/pb/demo1/demo.pb.go +protoc -I=./ --go_out=plugins=grpc:./demo/ ./demo.proto + +#生成demo.proto 示例2 +# 说明基本同示例1,最后包名为demo1 +# 但由于添加--go_opt=paths=source_relative,则会在相对路径下升级pb.go文件 +# pb代码文件路径为: ./demo/demo.pb.go +protoc -I=./ --go_out=plugins=grpc:./demo/ --go_opt=paths=source_relative ./demo.proto +``` + +#### 4.总结 + +本文介绍了定义proto并生成pb.go的过程。需要注意: + +基础包在定义proto协议时,需要明确指定**go_package**的路径。 + +从而其他包在引用基础包时, 生成的pb.go文件import才会是合理的路径,否则在实际落地中会找不到依赖包。 \ No newline at end of file diff --git a/markdown/2021/20210618-http2.0.md b/markdown/2021/20210618-http2.0.md new file mode 100644 index 0000000..5cdc978 --- /dev/null +++ b/markdown/2021/20210618-http2.0.md @@ -0,0 +1,207 @@ +[TOC] + +## http2.0 + +### 1.http2.0介绍及特性 + +- 通过magic, SETTING帧进行握手建连 + +- 数据报文以二进制帧划分,帧是http2.0通信的最小单位 +- 二进制帧有不同的类型HAED,DATA,单次请求包含多个帧类型,并且每个帧包里请求的uniqId一致 +- 采用帧机制,静态表及头部索引,数据传输相比http1.1更加小而高效 +- 采用多路复用机制,极大提高传输性能 + +``` +基于二进制分帧层,HTTP2.0可以在共享TCP连接的基础上同时发送请求和响应。HTTP消息被分解为独立的帧, +而不破坏消息本身的语义,交错发出去,在另一端根据流标识符和首部将他们重新组装起来。 +``` + +- 由于多路复用,当以SSL加密传输数据包时,只需要进行SSL握手一次,极大提高性能 + +### 2.如何使用http2.0 + +HTTP/2协议握手分2种方式,一种叫h2,一种叫h2c(HTTP/2 without TLS)。 + +- h2要求必须使用TLS加密,在TLS握手期间会顺带完成HTTPS/2协议的协商,如果协商失败(比如客户端不支持或者服务端不支持),则会使用HTTPS/1继续后续通讯。 + +- h2c不使用TLS,而是多了一次基于HTTP协议的握手往返来完成向HTTP/2协议的升级,一般不建议使用。 + +#### 2.1 直接使用http2.0(h2) + +golang可以通过server端及client端的相关配置,直接使用http2.0: + +##### 2.1.1 server端 + +> 服务端只要开启https服务, 默认支持http2.0 + +如果我们是X509格式签名的证书,最好程序先做一下有效性校验: + +```go +// TLS证书解析验证 +if _, err = tls.LoadX509KeyPair(G_config.ServerPem, G_config.ServerKey); err != nil { + return common.ERR_CERT_INVALID +} +``` + +确认证书有效后,我们最终通过serverTLS传入证书和私钥,启动一个HTTPS/2服务: + +```go +// HTTP/2 TLS服务 +server = &http.Server{ + ReadTimeout: time.Duration(G_config.ServiceReadTimeout) * time.Millisecond, + WriteTimeout: time.Duration(G_config.ServiceWriteTimeout) * time.Millisecond, + Handler: mux, +} +// 监听端口 +if listener, err = net.Listen("tcp", ":" + strconv.Itoa(G_config.ServicePort)); err != nil { + return +} +// 传入证书,以TLS启动服务,便可使用http2.0 +go server.ServeTLS(listener, G_config.ServerPem, G_config.ServerKey) +``` + +##### 2.1.2 client端 + +客户端主要是配置Transport,所谓Transport就是底层的连接管理器,包括了协议的处理能力: + +```go +transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true,}, // 不向CA校验服务端证书 + MaxIdleConns: G_config.GatewayMaxConnection, + MaxIdleConnsPerHost: G_config.GatewayMaxConnection, + IdleConnTimeout: time.Duration(G_config.GatewayIdleTimeout) * time.Second, // 连接空闲超时 +} +``` + +我们需要使用[http2.ConfigureTransport](https://godoc.org/golang.org/x/net/http2#ConfigureTransport)来启动HTTPS/2特性: + +```go +// 启动HTTP/2协议 +http2.ConfigureTransport(transport) +``` + +最后将Transport配置给Client,负责底层的连接与协议管理: + +```go +// HTTP/2 客户端 +client = &http.Client{ + Transport: transport, + Timeout: time.Duration(G_config.GatewayTimeout) * time.Millisecond, // 请求超时 +} +// begin your business ... +``` + +#### 2.2 通过http1.1升级使用http2.0(h2c) + +客户端、服务器都需要升级才能支持HTTP 2.0,升级过程中就存在HTTP1.1、HTTP 2.0并存的情况,然而他们都使用的80端口,那么如何来选择使用什么协议通信呢? + +应用层协商协议(APLN:Aplication Layer Protocol Negotiation)就是为了解决这个问题的,通过协商来选择通信的协议: + +- 客户端发起请求,如果支持HTTP/2,则带upgrade头部: + + ``` + GET /page HTTP/1.1 + Host: server.example.com + Connection: Upgrade, HTTP2-Settings + Upgrade: HTTP/2.0 + HTTP2-Settings: (SETTINGS payload) + ``` + +- 服务器不支持,则拒绝升级,通过HTTP1.1返回响应 + + ``` + HTTP/1.1 200 OK + Content-length: 243 + Content-type: text/html + (... HTTP 1.1 response ...) + ``` + +- 服务器支持,则接受升级,切换到新分帧,使用HTTP/2通信。 + + ``` + HTTP/1.1 101 Switching Protocols + Connection: Upgrade + Upgrade: HTTP/2.0 + (... HTTP 2.0 response ...) + ``` + +如上使用协议协商,无论是哪一种情况,都不需要额外的往返,如果客户端通过记录或者其他方式,知道服务器支持HTTP/2,则直接使用HTTP/2通信,无需再协议协商。 + +### 3.http2.0是否需要连接池? + +#### 3.1 http2.0流量控制 + +1. 流量控制是特定于一个连接的。 +2. 流量控制是基于**WINDOW_UPDATE帧**的。由接收方确定每个流以及整个连接上分别接收多少字节。 +3. 流量控制是有方向的,由接收者全面控制。接收方可以为每个流和整个连接设置任意的窗口大小。发送方必须尊重接收方设置的流量控制限制。 +4. 流量控制窗口的初始值是65535字节。 +5. 帧的类型决定了流量控制是否适用于帧。目前,只有DATA帧服从流量控制,所有其它类型的帧并不消耗流量控制窗口的空间。这保证了重要的控制帧不会被流量控制阻塞。 +6. HTTP/2只定义了WINDOW_UPDATE帧的格式和语义,具体实现可以选择任何满足需求的算法。 + +``` +nginx选择在接收窗口小于窗口最大值1/4时发送WINDOW_UPDATE帧,并且将窗口大小增长到最大值2^31-1。 +并不是所有服务器都这样实现的。 +比如有的实现是收到一个DATA帧,马上返回一个WINDOW_UPDATE帧,增长的值就是DATA帧的大小 +``` + +#### 3.2 TCP滑动窗口及拥塞机制 + +TCP必需要解决的可靠传输以及包乱序(reordering)的问题,所以需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。 + +##### 3.2.1 滑动窗口 + +TCP引入了一些技术和设计来做网络流控,Sliding Window是其中一个技术。TCP头里有一个字段叫Window,又叫Advertised-Window,这个字段是告诉发送端,接收方还有多少缓冲区可以接收数据。于是发送端就可以根据接收端的处理能力来发送数据,而不会导致接收端无法处理。 + +
+ +
+ +上图,我们可以看到一个处理缓慢的Server(接收端)是怎么把Client(发送端)的TCP Sliding Window给降成0的。如果Window变成0了,是不是发送端就不发数据了?是的,发送端就不发数据了,你可以想像成“Window Closed”,那你一定还会问,如果发送端不发数据了,接收方一会儿Window size 可用了,怎么通知发送端呢? + +解决这个问题,TCP使用了Zero Window Probe技术,缩写为ZWP,也就是说,发送端在窗口变成0后,会发ZWP的包给接收方,让接收方来ack他的Window尺寸,一般这个值会设置成3次,第次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会发RST把链接断了。 + +##### 3.2.2 拥塞机制 + +> 拥塞机制是从MTU的角度,以数据段MSS(Max Segment Size)的传输及ack根据一定算法实现的。 +> +> 如果你的网络包可以塞满MTU,那么你可以用满整个带宽,如果不能,那么你就会浪费带宽。 + +TCP通过Sliding Window来做流控(Flow Control),但是还不够。如果网络上的延时突然增加,TCP的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,就会进入恶性循环被不断地放大。如果一个网络内有成千上万的TCP连接都这么行事,那么马上就会形成“网络风暴”,TCP这个协议就会拖垮整个网络。 + +所以TCP实现了拥塞控制,主要是四个算法:**1) 慢启动;2) 拥塞避免;3) 拥塞发生;4) 快速恢复** + +这里简单介绍下**慢启动算法**: + +``` +在TCP拥塞避免中,每发送拥塞窗口值个数的TCP数据段(有效数据承载MSS),并且全部收到发送方对这些数据的ACK确认,我们就称完成了1个传输轮次(所谓的一轮次,也就是一个RTT时间) + +例如,拥塞窗口=4,当发送方发送了4个TCP报文段,并收到这4个TCP报文段的ACK确认,我们就称完成了一个传输轮次 +``` + +慢启动机制规定: + +- 拥塞窗口的初始值为1个MSS +- 每收到1个ACK确认,cwnd++ ;呈线性上升 +- 每过了1个传输轮次,cwnd = cwnd*2; 呈指数让升 +- 阈值ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法” + +
+ +
+ +#### 3.3.3 连接池? + +是否需要连接池取决于**http2.0的流量控制**以及**TCP协议的拥塞机制**。 + +为了提高程序鲁棒性,http2.0依然需要少量的连接池,但是如果TCP拥塞机制和http2.0 flow control的配置合理,能够让单个连接能够使用完整的通道容量,不用连接池也可以。 + +### 4.潜在问题 + +- 由于http2.0采用长连接,当RS端采用使用LVS(DR)暴露时,如果使用多路复用,只用一条连接时,则会导致RS的负载均衡受到影响 + +#### 参考 + +- HTTP2 多路复用:细节决定成败:https://blog.codavel.com/http2-multiplexing +- Do we still need a connection pool for microservices talking HTTP2?:https://stackoverflow.com/questions/55985658/do-we-still-need-a-connection-pool-for-microservices-talking-http2 +- Golang使用HTTP/2的正确方法: https://www.cnblogs.com/gao88/p/9824217.html +- TCP的流迭、拥塞处理:https://www.jianshu.com/p/b25cdf09ece2 \ No newline at end of file diff --git "a/markdown/2021/20210620-\345\272\224\347\224\250\347\250\213\345\272\217TCP\344\274\240\350\276\223\347\256\200\350\277\260.md" "b/markdown/2021/20210620-\345\272\224\347\224\250\347\250\213\345\272\217TCP\344\274\240\350\276\223\347\256\200\350\277\260.md" new file mode 100644 index 0000000..96f4ed3 --- /dev/null +++ "b/markdown/2021/20210620-\345\272\224\347\224\250\347\250\213\345\272\217TCP\344\274\240\350\276\223\347\256\200\350\277\260.md" @@ -0,0 +1,137 @@ +[TOC] + +### 应用程序TCP传输简述 + +> 本文概要讲述应用程序以TCP协议发送数据的流程 + +#### 1. 网络体系结构 + +OSI(七层)及TCP/IP(四层)网络体系结构如下图: + +
+ +
+ +本文以TCP/IP(四层)体系结构讲解,整体流程如下: + +- 应用层,将数据传输写入到TCP层缓冲区 +- TCP层,将数据加上TCP的头(端口等)信息,打包成Segment(段),发送到IP层 +- IP层,将数据加上ip相关信息,打包成Packet(包),发送给网络接口层 +- 网络接口层将数据报文加上校验等信息, 打包成帧(Frame),通过网卡设备发送 + +下面将详细介绍每一层。 + +#### 2.应用层 + +应用层会将数据写入到TCP层的 buffer缓冲区,一般linux系统默认为4kb. + +```bash +#TCP读缓冲区 +#第一个值:4096, 给socket接收缓冲区分配的最小值min +#第二个值:87380, 接收缓冲区大小在系统负载不重的情况下可以增长到这个值 +#第三个值:6291456, 接收缓冲区最大值max +king@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_rmem +4096 87380 6291456 + +#TCP写缓冲区,同上 +king@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_wmem +4096 16384 4194304 +``` + +宏观上,当buffer缓冲区写满时,TCP会将数据发送到对端: + +如果此时对端端口异常或关闭,会回复RST报文。而此时应用层无法感知。 + +假设应用层再次向buffer缓冲区写数据,则**会收到异常报错**。如果是golang程序则会引起panic + +#### 3.TCP层 + +TCP层通过滑动窗口进行数据报文发送,TCP协议头里有个字段叫Window,又叫Advertised-Window,这个字段是**接收端**告诉发送端自己还有多少缓冲区可以接收数据。 + +
+ +
+ +图中,我们可以看到: + +- 接收端LastByteRead指向了TCP缓冲区中读到的位置,NextByteExpected指向的地方是收到的连续包的最后一个位置,LastByteRcved指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。 + +- 发送端的LastByteAcked指向了被接收端Ack过的位置(表示成功发送确认),LastByteSent表示发出去了,但还没有收到成功确认的Ack,LastByteWritten指向的是上层应用正在写的地方。 + +TCP需要将window 窗口大小设置最优化,才能最大提高传输效率。window size通常会以MSS(Max Segment Size) 的大小,根据拥塞机制进行动态设定。RFC定义MSS的默认值是536。 + +如何才能使window size最优?则要避免对小的window size做出响应,直到有足够大的size再响应,这个思路可以同时实现在sender和receiver两端: + +- Receiver端优化: + + ``` + 如果收到的数据导致window size小于某个值,可以直接ack(0)回sender,这样就把window给关闭了, + 也阻止了sender再发数据过来。等到receiver端处理了一些数据后windows size 大于等于了MSS, + 或者receiver buffer有一半为空,就可以把window打开让send发送数据过来。 + ``` + +- Sender端优化: + + ``` + 1)要等到 Window Size>=MSS 或是 Data Size >=MSS + 2)收到之前发送数据的ack回包,才会发数据,否则就是在攒数据。 + ``` + +TCP层同时实现了拥塞控制机制,来保证在网络环境的不确定下,数据最优化传输。 + +拥塞控制主要是四个算法:**1)慢启动**,**2)拥塞避免**,**3)拥塞发生**,**4)快速恢复**。 + +简要介绍慢启动的算法如下(cwnd全称Congestion Window): + +1)连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。 + +2)每当收到一个ACK,cwnd++; 呈线性上升 + +3)每当过了一个RTT,cwnd = cwnd*2; 呈指数让升 + +4)还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法” + +所以,我们可以看到,如果网速很快的话,ACK也会返回得快,RTT也会短,那么,这个慢启动就一点也不慢: + +
+ +
+TCP层将数据按照MSS进行分割, **每个Segment单独添加序列号等头部信息,并打包成Segment** ,通过拥塞控制机制,根据窗口大小,同时发送一个或多个Segment给IP层。 + +**注意不是每个Tcp Segment都需要ack响应,根据滑动窗口策略来定。** + +#### 4.IP层 + +IP协议认为自己所处的环境是不可靠的,比如路由器坏掉、电缆断裂等,所以IP数据包在传输过程中如果出现错误,那么IP数据包会直接被丢弃,没有重试之类的补救措施,IP协议保持较为简单的处理流程,更加复杂的数据可靠性控制较由高层协议处理。 + +IP层添加src及dst的ip地址后,将每个TCP Segment打包成packet,发送给数据链路层。 + +#### 5.数据链路层/物理层 + +**网卡设备**包括数据链路层和物理层。MTU位于数据链路层的配置。以太网卡的MTU大小默认为1500bytes(MSS)。 + +数据链路层会通过目标IP地址进行ARP广播,获取到目标端MAC地址。将源及目标mac地址进行打包成帧(Frame),发送给物理层。 + +MTU为1500,一般不建议修改。因为在源端和目标端整个链路,每个传输节点(路由器,交换机)都设有MTU。单纯的修改自身并不会起到优化作用。 + +#### 6.百兆/千兆网卡 + +百兆/千兆网卡指的是网卡拆包,传输的能力。但是机器的瓶颈并不仅限于网卡,PCI总线可能只有833M。所以要适配,才会有更好性能 + +#### 7.带宽 + +网络线路分层: + +- **主干高速(核心网)** + +- **支线高速(传输网)** + +- **普通公路 (接入网)** + +而带宽一般是运营商在**接入网**进行硬件+软件限制(通过分光器等),一旦用户流量跨过接入网,就不再限速。 + +#### 参考 + +- TCP 的那些事儿:https://coolshell.cn/articles/11609.html + +- 运营商对带宽进行限制的原理是怎样的?:https://www.zhihu.com/question/19811707 \ No newline at end of file diff --git "a/markdown/2021/20210716-\344\275\277\347\224\250nginx\346\255\243\345\220\221\344\273\243\347\220\206https.md" "b/markdown/2021/20210716-\344\275\277\347\224\250nginx\346\255\243\345\220\221\344\273\243\347\220\206https.md" new file mode 100644 index 0000000..4d0d390 --- /dev/null +++ "b/markdown/2021/20210716-\344\275\277\347\224\250nginx\346\255\243\345\220\221\344\273\243\347\220\206https.md" @@ -0,0 +1,230 @@ +[TOC] + +## 使用nginx正向代理https + +本文主要探讨如何使用nginx实现https代理。 + +### 1.L7 解决方案 + +#### 1.1 原理 + +L7 是通过HTTP CONNECT协议,在客户和代理服务器之间建立隧道,然后将客户端的数据通过代理服务器之间透传到服务端。 + +
+ +
+说明如下: + +- (1) 客户端向代理服务器发送 HTTP CONNECT 请求。 +- (2) 代理服务器利用HTTP CONNECT请求中的主机和端口信息与目标服务器建立TCP连接。 +- (3) 代理服务器向客户端返回 HTTP 200 响应。 +- (4) 客户端与代理服务器建立HTTP CONNECT隧道。HTTPS流量到达代理服务器后,代理服务器通过TCP连接将HTTPS流量透传到远程目标服务器。代理服务器只透传HTTPS流量,不解密HTTPS流量。 + +#### 1.2 环境搭建 + +nginx官方并不支持HTTP CONNECT 方法,需要安装 [ngx_http_proxy_connect_module](https://github.com/chobits/ngx_http_proxy_connect_module) 扩展来支持HTTP CONNECT 。 + +##### 1.2.1 安装nginx + +```bash +#1.下载nginx包: http://nginx.org/en/download.html +wget http://nginx.org/download/nginx-1.14.2.tar.gz + +#2.下载nginx_http_proxy_connect_module扩展,进行打补丁 +patch -p1 < /path/to/ngx_http_proxy_connect_module/patch/proxy_connect.patch + +#3.编译nginx +./configure \ +--prefix=/usr/local/nginx \ +--user=www \ +--group=www \ +--with-http_ssl_module \ +--with-http_stub_status_module \ +--with-http_realip_module \ +--with-threads \ +--add-module=/root/path/ngx_http_proxy_connect_module-master/ + +#4.编译,并将二进制拷贝到目标文件夹 +make +cp objs/nginx /usr/local/nginx/sbin/nginxconnect + +#5.查看版本 +/usr/local/nginx/sbin/nginxconnect -V +``` + +##### 1.2.2 配置nginx.conf + +```bash +server { + listen 443; + # dns resolver used by forward proxying + resolver 8.8.8.8; + access_log /data/log/nginx/access_proxy-443.log main; + # forward proxy for CONNECT request + proxy_connect; + proxy_connect_allow 443; + proxy_connect_connect_timeout 10s; + proxy_connect_read_timeout 60s; + proxy_connect_send_timeout 60s; + } +``` + +#### 1.3 使用方式 + +```bash +#指定使用代理服务器 +curl -v --proxy 10.11.12.13:443 --location --request GET 'https://www.google.com' + +* About to connect() to proxy 10.11.12.13 port 443 (#0) +* Trying 10.11.12.13... connected +* Connected to 10.11.12.13 (10.11.12.13) port 443 (#0) +* Establish HTTP proxy tunnel to www.google.com:443 +> CONNECT www.google.com:443 HTTP/1.1 +> Host: www.google.com:443 +> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.15.3 zlib/1.2.3 libidn/1.18 libssh2/1.4.2 +> Proxy-Connection: Keep-Alive +> +< HTTP/1.1 200 Connection Established +< Proxy-agent: nginx +< +* Proxy replied OK to CONNECT request +* Initializing NSS with certpath: sql:/etc/pki/nssdb +* CAfile: /etc/pki/tls/certs/ca-bundle.crt + CApath: none +* SSL connection using TLS_RSA_WITH_AES_128_CBC_SHA +* Server certificate: +* subject: CN=www.google.com +* start date: Jun 22 16:06:10 2021 GMT +* expire date: Sep 14 16:06:09 2021 GMT +* common name: www.google.com +* issuer: CN=GTS CA 1C3,O=Google Trust Services LLC,C=US +> GET / HTTP/1.1 +> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.15.3 zlib/1.2.3 libidn/1.18 libssh2/1.4.2 +> Host: www.google.com +> Accept: */* +> ... +``` + +`-v`参数打印请求详细信息,客户端首先与代理服务器`10.11.12.13`建立HTTP CONNECT隧道。一旦代理回复`HTTP/1.1 200 Connection Established`,客户端就会发起 TLS/SSL 握手并向服务器发送流量。 + +### 2.L4 解决方案 + +如果想要在TCP层使用nginx流作为HTTPS流量的代理,则会遇到应该如何获取到域名的问题。 +因为TCP层获取的信息仅限于IP地址和端口,所以无法获取到客户端想要访问的目标域名。 +为了获得目标域名,代理必须能够从上层报文中提取域名。 + +> nginx流不是严格意义上的L4代理,它必须寻求上层的帮助才能提取域名。 + +#### 2.1 ngx_stream_ssl_preread_module + +为了在不解密HTTPS流量的情况下获取HTTPS流量的目标域名,唯一的方法是使用TLS/SSL握手时第一个ClientHello报文中包含的SNI字段。 + +从 1.11.5 版本开始,nginx支持[`ngx_stream_ssl_preread_module`](http://nginx.org/en/docs/stream/ngx_). 该模块有助于从 ClientHello 数据包中获取 SNI 和 ALPN。然而这也带来了一个限制,即所有客户端必须在 TLS/SSL 握手期间在 ClientHello 数据包中包含 SNI 字段。否则,nginx流代理将不知道客户端需要访问的目标域名。 + +#### 2.2 环境搭建 + +##### 2.2.1 安装nginx + +```bash +#1.编译nginx +./configure \ +--prefix=/usr/local/nginx \ +--user=www \ +--group=www \ +--with-http_ssl_module \ +--with-http_stub_status_module \ +--with-http_realip_module \ +--with-threads \ +--with-stream \ +--with-stream_ssl_preread_module \ +--with-stream_ssl_module + +#2.编译,并将二进制拷贝到目标文件夹 +make +cp objs/nginx /usr/local/nginx/sbin/nginxstream + +#3.查看版本 +/usr/local/nginx/sbin/nginxstream -V +``` + +##### 2.2.2 配置nginx.conf + +```bash +stream { + + resolver 8.8.8.8; + + log_format main '[$time_local] - request_addr:$remote_addr ' + 'protocol:$protocol status:$status bytes_sent:$bytes_sent bytes_received:$bytes_received ' + 'session_time:$session_time upstream_addr:"$upstream_addr" ' + 'upstream_bytes_sent:"$upstream_bytes_sent" upstream_bytes_received:"$upstream_bytes_received" upstream_connect_time:"$upstream_connect_time"'; + + server { + access_log /data/log/nginx/access_proxy_stream-443.log main; + listen 443; + ssl_preread on; + proxy_connect_timeout 10s; + proxy_pass $ssl_preread_server_name:$server_port; + } +} +``` + +#### 2.3 使用方式 + +作为L4转发代理,nginx基本上是将流量透传到上层,不需要HTTP CONNECT建立隧道。因此,L4 方案适用于透明代理模式。例如,当目标域名通过DNS解析的方式定向到代理服务器时,就需要通过绑定`/etc/hosts`到客户端模拟透明代理模式。 + +```bash +cat /etc/hosts +... +# 把域名www.google.com绑定到正向代理服务器10.11.12.13 +10.11.12.13 www.google.com + +#配好hosts,直接请求https +curl -v --location --request GET 'https://www.google.com' +* About to connect() to www.google.com port 443 (#0) +* Trying 10.11.12.13... connected +* Connected to www.google.com (10.11.12.13) port 443 (#0) +* Initializing NSS with certpath: sql:/etc/pki/nssdb +* CAfile: /etc/pki/tls/certs/ca-bundle.crt + CApath: none +* SSL connection using TLS_RSA_WITH_AES_128_CBC_SHA +* Server certificate: +* subject: CN=www.google.com +* start date: Jun 22 16:06:10 2021 GMT +* expire date: Sep 14 16:06:09 2021 GMT +* common name: www.google.com +* issuer: CN=GTS CA 1C3,O=Google Trust Services LLC,C=US +> GET / HTTP/1.1 +> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.15.3 zlib/1.2.3 libidn/1.18 libssh2/1.4.2 +> Host: www.google.com +> Accept: */* +> +``` + +#### 2.4 常见问题 + +##### 2.4.1 客户端手动设置代理导致访问失败 + +客户端尝试在 nginx之前建立 HTTP CONNECT 隧道。但是,由于 nginx对流量进行透传,因此 CONNECT 请求会直接转发到目标服务器。目标服务器不接受 CONNECT 方法。因此`Proxy CONNECT aborted`反映在上面的片段中,导致访问失败。 + +``` +curl -v --proxy 10.11.12.13:443 --location --request GET 'https://www.google.com' +* About to connect() to proxy 10.11.12.13 port 443 (#0) +* Trying 10.11.12.13... connected +* Connected to 10.11.12.13 (10.11.12.13) port 443 (#0) +* Establish HTTP proxy tunnel to www.google.com:443 +> CONNECT www.google.com:443 HTTP/1.1 +> Host: www.google.com:443 +> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.15.3 zlib/1.2.3 libidn/1.18 libssh2/1.4.2 +> Proxy-Connection: Keep-Alive +> +* Proxy CONNECT aborted +* Closing connection #0 +curl: (56) Proxy CONNECT aborted +``` + + + +#### 参考 + +- https://www.alibabacloud.com/blog/how-to-use-nginx-as-an-https-forward-proxy-server_595799 \ No newline at end of file diff --git "a/markdown/2021/20211228-docker\344\271\213iptables.md" "b/markdown/2021/20211228-docker\344\271\213iptables.md" new file mode 100644 index 0000000..6d39080 --- /dev/null +++ "b/markdown/2021/20211228-docker\344\271\213iptables.md" @@ -0,0 +1,445 @@ +[TOC] + +## docker 之 iptables + +### 1.背景 + +在linux(centos 7.8.2003)宿主机里安装docker(19.03.13)环境,基于centos:centos7.4.1708镜像启动了docker容器,并在此容器里部署kubernetes环境单机版。 + +
+ +
+ +启动docker容器: + +```bash +#使用--privileged开启特权,拥有真正root权限 +#启动时执行/usr/sbin/init, 能够启动dbus-daemon(进程间通信服务,k8s需要用到) +#预留11101->11104端口,用于测试 +docker run -itd --privileged -p 11100:11100 -p 11101:11101 -p 11102:11102 -p 11103:11103 -p 11104:11104 --name yushaolong-kube centos:centos7.4.1708 /usr/sbin/init +``` + +进入docker容器,并部署kubernetes环境(k8s in docker) + +```bash +#1.安装etcd,kubernetes +yum install -y etcd kubernetes + +#2.修改相关配置 +#2.1 +> vim /etc/sysconfig/docker +OPTIONS='--selinux-enabled=false --insecure-registry gcr.io' +#2.2 +> vim /etc/kubernetes/apiserver +KUBE_ADMISSION_CONTROL 去除 --admission_control中的ServiceAccount +#2.3 修改容器驱动 +> vim /etc/sysconfig/docker-storage +修改 DOCKER_STORAGE_OPTIONS="--storage-driver overlay " 为 DOCKER_STORAGE_OPTIONS="--storage-driver devicemapper " +#2.4 添加网卡 +> for i in `seq 0 6`;do mknod -m 0660 /dev/loop$i b 7 $i;done + +#3.修改proxy配置 +#因为kube-proxy无法正常启动,可通过 journalctl -f -u kube-proxy 查看错误日志 +> vim /etc/kubernetes/proxy +KUBE_PROXY_ARGS="--conntrack-max-per-core=0" + +#4.按顺序,启动kubernetes,并检查各进程是否正常 +> systemctl start etcd +> systemctl start docker +> systemctl start kube-apiserver +> systemctl start kube-controller-manager +> systemctl start kube-scheduler +> systemctl start kubelet +> systemctl start kube-proxy +``` + +### 2.kubernetes项目 + +#### 2.1 部署架构 + +该项目较为简单,主要包括frontend和redis两个服务模块: + +
+ +
+ +#### 2.2 服务状态 + +##### 2.2.1 pod信息 + +```bash +> kubectl get pod -o wide +NAME READY STATUS RESTARTS AGE IP NODE +frontend-071f3 1/1 Running 0 3d 172.18.0.5 127.0.0.1 +frontend-mrhtc 1/1 Running 0 3d 172.18.0.7 127.0.0.1 +frontend-shrj4 1/1 Running 0 3d 172.18.0.6 127.0.0.1 +redis-master-n01n8 1/1 Running 0 3d 172.18.0.2 127.0.0.1 +redis-slave-g08mq 1/1 Running 0 3d 172.18.0.3 127.0.0.1 +redis-slave-qqv9z 1/1 Running 0 3d 172.18.0.4 127.0.0.1 +``` + +##### 2.2.2 副本信息 + +```bash +> kubectl get rc +NAME DESIRED CURRENT READY AGE +frontend 3 3 3 3d +redis-master 1 1 1 3d +redis-slave 2 2 2 3d +``` + +##### 2.2.3 service信息 + +```bash +> kubectl get svc +NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE +frontend 10.254.6.77 80:11103/TCP 17h #暴露了11103端口 +kubernetes 10.254.0.1 443/TCP 9d +redis-master 10.254.74.37 6379/TCP 3d +redis-slave 10.254.178.201 6379/TCP 3d +``` + +### 3.问题现象 + +在docker容器环境,请求frontend的service服务,可以成功获取响应结果 + +```bash +#1.通过127.0.0.1方式 +> curl 127.0.0.1:11103 + +... 内容 ... + + +#2.通过本地k8s的service地址 +> curl 10.254.6.77:80 + +... 内容 ... + +``` + +但是在宿主机上请求相应service服务,则失败: + +```bash +#1.通过127.0.0.1方式 +#由于已经对docker容器做了相应端口映射11101->11104 +> curl 127.0.0.1:11103 +curl: (7) Failed connect to 172.17.0.2:11103; No route to host + +#2.通docker0网桥请求centos容器(172.17.0.2)内部 +#docker0网桥 +> ifconfig docker0 +docker0: flags=4163 mtu 1500 + inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255 + inet6 fe80::42:29ff:fe97:501b prefixlen 64 scopeid 0x20 +#请求容器端口 +> curl 172.17.0.2:11103 +curl: (7) Failed connect to 172.17.0.2:11103; No route to host +``` + +### 4.原因分析 + +#### 4.1 使用iptables查看规则表 + +由于宿主机可以ping通docker容器,所以问题出现在规则过滤表中,可在docker容器内,通过`iptables`命令查看。 + +```bash +#在docker容器内部操作 +#1.查看filter表 +> iptables -nvL +Chain INPUT (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + 36M 17G KUBE-FIREWALL all -- * * 0.0.0.0/0 0.0.0.0/0 + 36M 17G ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED + 13 1092 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 +43765 2626K ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 + 1 60 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 + 113 6860 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited + +Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + 408K 28M DOCKER-ISOLATION all -- * * 0.0.0.0/0 0.0.0.0/0 + 408K 28M DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0 + 359K 25M ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED + 0 0 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0 +48707 2922K ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0 + 86 5200 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited + +Chain OUTPUT (policy ACCEPT 16 packets, 832 bytes) + pkts bytes target prot opt in out source destination + 14M 6368M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ + 36M 16G KUBE-FIREWALL all -- * * 0.0.0.0/0 0.0.0.0/0 + +Chain DOCKER (1 references) + pkts bytes target prot opt in out source destination + +Chain DOCKER-ISOLATION (1 references) + pkts bytes target prot opt in out source destination + 408K 28M RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 + +Chain KUBE-FIREWALL (2 references) + pkts bytes target prot opt in out source destination + 0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes firewall for dropping marked packets */ mark match 0x8000/0x8000 + +Chain KUBE-SERVICES (1 references) + pkts bytes target prot opt in out source destination + + +#2.查看nat表 +> iptables -nvL -t nat +Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + 8298 498K KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ + 129 8188 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL + +Chain INPUT (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + +Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination +27933 1677K KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ + 18 1128 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL + +Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + 9806 589K MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 + 1 84 MASQUERADE all -- * !docker0 172.18.0.0/16 0.0.0.0/0 +27651 1660K KUBE-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */ + +Chain DOCKER (2 references) + pkts bytes target prot opt in out source destination + 0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0 + +Chain KUBE-MARK-DROP (0 references) + pkts bytes target prot opt in out source destination + 0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x8000 + +Chain KUBE-MARK-MASQ (8 references) + pkts bytes target prot opt in out source destination + 0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000 + +Chain KUBE-NODEPORTS (1 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-MARK-MASQ tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/frontend: */ tcp dpt:11103 + 0 0 KUBE-SVC-GYQQTB6TY565JPRW tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/frontend: */ tcp dpt:11103 + +Chain KUBE-POSTROUTING (1 references) + pkts bytes target prot opt in out source destination + 0 0 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000 + +Chain KUBE-SEP-5ZUVGKEDQRTZFI3V (2 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-MARK-MASQ all -- * * 172.17.0.2 0.0.0.0/0 /* default/kubernetes:https */ + 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/kubernetes:https */ recent: SET name: KUBE-SEP-5ZUVGKEDQRTZFI3V side: source mask: 255.255.255.255 tcp to:172.17.0.2:6443 + +Chain KUBE-SEP-75B5J3OSVS6TQNUC (1 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-MARK-MASQ all -- * * 172.18.0.4 0.0.0.0/0 /* default/redis-slave: */ + 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/redis-slave: */ tcp to:172.18.0.4:6379 + +Chain KUBE-SEP-AEX7DA47UPXHK3H4 (1 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-MARK-MASQ all -- * * 172.18.0.5 0.0.0.0/0 /* default/frontend: */ + 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/frontend: */ tcp to:172.18.0.5:80 + +Chain KUBE-SEP-EV6E4CZ7RULJ5ODK (1 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-MARK-MASQ all -- * * 172.18.0.6 0.0.0.0/0 /* default/frontend: */ + 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/frontend: */ tcp to:172.18.0.6:80 + +Chain KUBE-SEP-EV7MVTELAJCKTBBY (1 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-MARK-MASQ all -- * * 172.18.0.3 0.0.0.0/0 /* default/redis-slave: */ + 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/redis-slave: */ tcp to:172.18.0.3:6379 + +Chain KUBE-SEP-HYIOT7CVE52G3BVC (1 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-MARK-MASQ all -- * * 172.18.0.7 0.0.0.0/0 /* default/frontend: */ + 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/frontend: */ tcp to:172.18.0.7:80 + +Chain KUBE-SEP-ZULUBSZ2OTAW7A27 (1 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-MARK-MASQ all -- * * 172.18.0.2 0.0.0.0/0 /* default/redis-master: */ + 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/redis-master: */ tcp to:172.18.0.2:6379 + +Chain KUBE-SERVICES (2 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-SVC-AGR3D4D4FQNH4O33 tcp -- * * 0.0.0.0/0 10.254.178.201 /* default/redis-slave: cluster IP */ tcp dpt:6379 + 0 0 KUBE-SVC-GYQQTB6TY565JPRW tcp -- * * 0.0.0.0/0 10.254.6.77 /* default/frontend: cluster IP */ tcp dpt:80 + 0 0 KUBE-SVC-NPX46M4PTMTKRN6Y tcp -- * * 0.0.0.0/0 10.254.0.1 /* default/kubernetes:https cluster IP */ tcp dpt:443 + 0 0 KUBE-SVC-7GF4BJM3Z6CMNVML tcp -- * * 0.0.0.0/0 10.254.74.37 /* default/redis-master: cluster IP */ tcp dpt:6379 + 0 0 KUBE-NODEPORTS all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL + +Chain KUBE-SVC-7GF4BJM3Z6CMNVML (1 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-SEP-ZULUBSZ2OTAW7A27 all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/redis-master: */ + +Chain KUBE-SVC-AGR3D4D4FQNH4O33 (1 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-SEP-EV7MVTELAJCKTBBY all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/redis-slave: */ statistic mode random probability 0.50000000000 + 0 0 KUBE-SEP-75B5J3OSVS6TQNUC all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/redis-slave: */ + +Chain KUBE-SVC-GYQQTB6TY565JPRW (2 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-SEP-AEX7DA47UPXHK3H4 all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/frontend: */ statistic mode random probability 0.33332999982 + 0 0 KUBE-SEP-EV6E4CZ7RULJ5ODK all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/frontend: */ statistic mode random probability 0.50000000000 + 0 0 KUBE-SEP-HYIOT7CVE52G3BVC all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/frontend: */ + +Chain KUBE-SVC-NPX46M4PTMTKRN6Y (1 references) + pkts bytes target prot opt in out source destination + 0 0 KUBE-SEP-5ZUVGKEDQRTZFI3V all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/kubernetes:https */ recent: CHECK seconds: 10800 reap name: KUBE-SEP-5ZUVGKEDQRTZFI3V side: source mask: 255.255.255.255 + 0 0 KUBE-SEP-5ZUVGKEDQRTZFI3V all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/kubernetes:https */ + +``` + +规则表说明: + +- `ADDRTYPE match dst-type LOCAL`: 表示目的地址仅为本地回环时命中该条件 +- `MASQUERADE`: 用于POSTROUTING,基于SNAT的源地址动态映射为本地eth0地址 +- `MARK`: 标记报文并继续执行当前规则链 +- `RETURN`: 终止当前规则链,执行下一条 +- `DNAT`: 用于PREROUTING,目标地址TCP头部ip内容替换为映射地址 +- `RELATED,ESTABLISHED,NEW`: NEW表示tcp连接的第一次握手包;ESTABLISHED表示已建立的连接包;RELATED用于复杂场景的关联连接 + +#### 4.2 iptables执行链 + +
+ +
+ +#### 4.3 规则执行图解 + +由于本例中iptables不涉及mangle及raw表,所以简化规则流如下: + +
+ +
+ +在docker容器中执行`curl 127.0.0.1:11103`请求成功时,分析步骤如下: + +(1) 请求源地址及目标地址 + +``` +127.0.0.1:43327 -> 127.0.0.1:11103 +``` + +(2) 命中nat表PREROUTING规则DNAT + +``` +127.0.0.1:43327 -> 172.18.0.6:80 +``` + +(3) 命中filter表INPUT + +``` +43765 2626K ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 +``` + +(4) 应用层接收,处理并响应 + +``` +172.18.0.6:80 -> 127.0.0.1:43327 +``` + +(5) 命中nat表POSTROUTING规则MASQUERADE + +``` +127.0.0.1:11103 -> 127.0.0.1:43327 +``` + +#### 4.4 宿主机请求分析 + +宿主机请求`curl 172.17.0.2:11103`时, 响应`No route to host`: + +(1)无法命中nat表PREROUTING规则 + +```bash +#1.没有11103端口, 无法命中 +tcp dpt ${port} +#2.需要原地址为本地回环, 无法命中 +ADDRTYPE match dst-type LOCAL +``` + +(2)命中filter表INPUT规则 + +```bash +#命中最后一条REJECT规则 +Chain INPUT (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + 36M 17G KUBE-FIREWALL all -- * * 0.0.0.0/0 0.0.0.0/0 + 36M 17G ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED + 13 1092 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 +43765 2626K ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 + 1 60 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 + 113 6860 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited #拒绝不符合上述任何规则的数据包,并且发送一条host prohibited的消息 +``` + +### 5.解决方案 + +鉴于iptables规则配置的复杂性, 本例没有重新配置iptables规则表, 而是采用了较为简单的nginx代理方案。 + +#### 5.1 修改frontend svc端口 + +```bash +#在容器中执行: +#修改k8s service 端口为: 30003 +> kubectl get svc +NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE +frontend 10.254.6.77 80:30003/TCP 18h +kubernetes 10.254.0.1 443/TCP 10d +redis-master 10.254.74.37 6379/TCP 3d +redis-slave 10.254.178.201 6379/TCP 3d +``` + +#### 5.2 容器中部署nginx + +```bash +#nginx配置文件 +upstream backend { + server 127.0.0.1:30003; # 代理到30003端口 + keepalive 2000; +} +server { + listen 11103; #监听本地11103端口 + server_name 0.0.0.0; + client_max_body_size 1024M; + + location / { + proxy_pass http://backend/; + } +} +``` + +#### 5.3 修改iptables规则表 + +```bash +#在容器中执行: +#修改iptables filter INPUT规则 +#允许应用程序接收11103端口的报文 +iptables -I INPUT -p tcp --dport 11103 -j ACCEPT +``` + +#### 5.4 宿主机请求 + +```bash +#1.在宿主机请求容器中的端口,请求成功 +> curl 172.17.0.2:11103 + + ... 内容 ... + + +#2.在宿主机请求本地端口,请求成功 +> curl 127.0.0.1:11103 + + ... 内容 ... + +``` + +#### 5.5 规则执行图解 + +
+ +
+ +### 6.参考 + +- iptables No route to host: https://developer.aliyun.com/article/174058 \ No newline at end of file diff --git "a/markdown/2022/20220427-\345\234\250\346\265\217\350\247\210\345\231\250\345\234\260\345\235\200\346\240\217\350\276\223\345\205\245URL\344\274\232\350\277\233\350\241\214\345\223\252\344\272\233\346\212\200\346\234\257\346\255\245\351\252\244\357\274\237.pdf" "b/markdown/2022/20220427-\345\234\250\346\265\217\350\247\210\345\231\250\345\234\260\345\235\200\346\240\217\350\276\223\345\205\245URL\344\274\232\350\277\233\350\241\214\345\223\252\344\272\233\346\212\200\346\234\257\346\255\245\351\252\244\357\274\237.pdf" new file mode 100644 index 0000000..5f3fa1c Binary files /dev/null and "b/markdown/2022/20220427-\345\234\250\346\265\217\350\247\210\345\231\250\345\234\260\345\235\200\346\240\217\350\276\223\345\205\245URL\344\274\232\350\277\233\350\241\214\345\223\252\344\272\233\346\212\200\346\234\257\346\255\245\351\252\244\357\274\237.pdf" differ diff --git "a/markdown/2022/20220428-\347\263\273\347\273\237\350\260\203\347\224\250\345\217\212\344\270\212\344\270\213\346\226\207\345\210\207\346\215\242\346\200\247\350\203\275\345\210\206\346\236\220.md" "b/markdown/2022/20220428-\347\263\273\347\273\237\350\260\203\347\224\250\345\217\212\344\270\212\344\270\213\346\226\207\345\210\207\346\215\242\346\200\247\350\203\275\345\210\206\346\236\220.md" new file mode 100644 index 0000000..c10ad26 --- /dev/null +++ "b/markdown/2022/20220428-\347\263\273\347\273\237\350\260\203\347\224\250\345\217\212\344\270\212\344\270\213\346\226\207\345\210\207\346\215\242\346\200\247\350\203\275\345\210\206\346\236\220.md" @@ -0,0 +1,151 @@ +[TOC] + +### 1.用户态和内核态如何切换 + +用户态特权等级Ring=3,内核态特权等级Ring=0。 + +以**alloc**函数为例,操作系统对外封装了更加安全的**malloc**函数,避免应用程序随意操作。 + +进程启动时,操作系统会开辟虚拟空间,将elf文件进行重定位,并装载到内存。 + +虚拟空间分为用户段和内核段,alloc函数的调用地址会被映射到内核段地址。 + +当进程调用malloc函数时,malloc内进行特权校验,切换特权等级,然后寄存器指针从用户段指向内核段alloc地址。 + +操作系统的内核空间地址对于所有进程,都是一致的。 + +### 2.进程/线程/协程切换的损耗 + +#### 2.1 进程切换损耗 + +直接开销就是在切换时,cpu必须作的事情,包括: + +- 一、切换页表全局目录 +- 二、切换内核态堆栈 +- 三、切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)网络 + - ip(instruction pointer):指向当前执行指令的下一条指令 + - bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址 + - sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址 + - cr3:页目录基址寄存器,保存页目录表的物理地址 + - ...... +- 四、刷新TLB +- 五、系统调度器的代码执行 + +间接开销主要指的是虽然切换到一个新进程后,因为各类缓存并不热,速度运行会慢一些。若是进程始终都在一个CPU上调度还好一些,若是跨CPU的话,以前热起来的TLB、L一、L二、L3由于运行的进程已经变了,因此以局部性原理cache起来的代码、数据也都没有用了,致使新进程穿透到内存的IO会变多。 + +#### 2.2 线程切换损耗 + +同进程间的线程切换,其开销主要来源于用户态与内核态之间的转化,因为当发生切换是,意味着发生了调度,以主调度器为例,需要首先触发中断,这就要进入内核态,切换完成后再回到用户态,这才是线程切换的主要开销 + +#### 2.3 协程切换的损耗 + +协程也就是操作系统原理里面的用户级线程,它的切换是发生在用户级的,单纯的进行栈的切换和对寄存器信息的保存,不会进行内核态的转化,时间开销是几微妙,因此用户级线程是实现高并发的重要思路,这也是Go语言设计的基石。 + +### 3.linux性能分析命令 + +(1) pstree: 将所有进程以树状图显示 + +```bash +> pstree -u root +init─┬─NetworkManager─┬─dhclient + │ └─{NetworkManager} + ├─abrtd + ├─acpid + ├─agetty + ├─atd + ├─bash───python───{python} + ├─console-kit-dae───63*[{console-kit-da}] + ├─crond +``` + +(2) ps -eLo pid,lwp,pcpu | grep ${pid} : 查看进程下所有线程cpu消耗 + +```bash +#查看进程下所有的线程 +> ps -eLf | grep ${pid} +UID PID PPID LWP C NLWP STIME TTY TIME CMD +root 4557 1 4557 0 18 Mar10 ? 03:01:51 ./bin/darwin-lavender +root 4557 1 4559 0 18 Mar10 ? 06:32:39 ./bin/darwin-lavender +root 4557 1 4560 0 18 Mar10 ? 04:24:21 ./bin/darwin-lavender +root 4557 1 4561 0 18 Mar10 ? 04:44:10 ./bin/darwin-lavender +root 4557 1 4562 0 18 Mar10 ? 04:42:43 ./bin/darwin-lavender +root 4557 1 4563 0 18 Mar10 ? 00:00:00 ./bin/darwin-lavender + +#查看进程下所有线程cpu消耗 +> ps -eLo pid,lwp,pcpu | grep ${pid} +PID LWP %CPU + 4557 4557 0.2 + 4557 4559 0.5 + 4557 4560 0.3 + 4557 4561 0.4 + 4557 4562 0.4 + 4557 4563 0.0 +``` + +(3) pstack: 查看线程调用栈 + +```bash +> pstack 4559 +Thread 1 (process 4559): +#0 0x0000000000463273 in runtime.futex () +#1 0x000000000042e524 in runtime.futexsleep () +#2 0x000000000040c90e in runtime.notetsleep_internal () +#3 0x000000000040c9e1 in runtime.notetsleep () +#4 0x000000000043d33e in runtime.sysmon () +#5 0x00000000004356b3 in runtime.mstart1 () +#6 0x00000000004355ce in runtime.mstart () +#7 0x0000000000401893 in runtime/cgo(.text) () +#8 0x0000000000000003 in ?? () +#9 0x0000000000000000 in ?? () +``` + +(4) vmstat: 查看系统上下文切换 + +```bash +#每秒输出一次,总共输出3次 +> vmstat 1 3 +procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu----- + r b swpd free buff cache si so bi bo in cs us sy id wa st + 3 0 0 657676 201516 3214040 0 0 0 11 0 0 1 1 98 0 0 + 0 0 0 657164 201516 3214040 0 0 0 4 5622 11336 4 2 94 0 0 + 0 0 0 657164 201516 3214044 0 0 0 0 4966 10735 3 2 96 0 0 +``` + +(5) mpstat: 查看各cpu运行状态 + +```bash +#每秒输出一次,总共输出3次 +#iowait:读写耗时占比 +#irq: 硬中断占比 +#soft: 软中断占比 +> mpstat -P ALL 1 3 +Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle +Average: all 0.17 0.00 0.34 0.84 0.00 0.00 0.00 0.00 0.00 98.65 +Average: 0 0.34 0.00 0.67 1.68 0.00 0.00 0.00 0.00 0.00 97.32 +Average: 1 0.00 0.00 0.33 0.00 0.00 0.33 0.00 0.00 0.00 99.33 +``` + +(6) pidstat: 查看线程的cpu, 上下文切换 + +```bash +# -w 表示输出进程的上下文切换情况; +# -u 表示输出进程cpu使用情况 +# -t 表示输出进程中线程统计信息 +# -p 指定进程pid +# 1 5: 每秒输出1次,输出5次 +> pidstat -w -u -t -p 4559 1 5 +Average: TGID TID cswch/s nvcswch/s Command +Average: 4567 - 0.00 0.00 darwin-lavender +Average: - 4557 24.80 0.20 |__darwin-lavender +Average: - 4559 676.40 0.80 |__darwin-lavender +Average: - 4560 134.20 55.60 |__darwin-lavender +Average: - 4561 94.20 33.20 |__darwin-lavender +Average: - 4562 32.00 0.00 |__darwin-lavender +Average: - 4563 0.00 0.00 |__darwin-lavender +``` + +#### 4.参考 + +线程切换: https://www.zhihu.com/question/490502122 + +cpu系统调用切换: https://www.zhihu.com/question/410762507/answer/2403485698 \ No newline at end of file diff --git "a/markdown/2022/20220503-linux\345\256\211\350\243\205\347\275\221\345\215\241\351\251\261\345\212\250.md" "b/markdown/2022/20220503-linux\345\256\211\350\243\205\347\275\221\345\215\241\351\251\261\345\212\250.md" new file mode 100644 index 0000000..9e5915e --- /dev/null +++ "b/markdown/2022/20220503-linux\345\256\211\350\243\205\347\275\221\345\215\241\351\251\261\345\212\250.md" @@ -0,0 +1,170 @@ +[TOC] + +### linux安装网卡驱动 + +#### 1.什么是驱动? +驱动是操作系统与硬件沟通的桥梁。操作系统定义了硬件接入的接口,驱动进行实现。 + +**应用程序读取网卡数据的流程大致如下:** + +- 1.基于网卡硬件型号,安装相应驱动 +- 2.驱动会向系统内核申请sk_buff空间,此空间位于驱动层,并将空间地址发送给网卡 +- 3.网卡接受数据包之后,通过DMA写入到sk_buff空间,并触发硬中断 +- 4.操作系统保存运行上下文,响应硬中断,并将信号写入软中断表 +- 5.基于操作系统的时间片调度方式,内核发现软中断之后,将数据从驱动层sk_buff读入到内核空间 +- 6.应用程序将数据从内核空间读入到用户空间 + +#### 2.命令概要 +`lsusb`: 查询插入到usb接口的硬件设备型号 + +`lspci`: 查询pci总线上的硬件设备型号 + +`lsmod`: 查询已安装的驱动模块 + +`modprobe`: 安装驱动 + +`ifconfig ${card} up`: 激活网卡 + +`iwlist ${card} scan`: 扫描无线网络 + +`wpa_passphrase`: 设置网络密码 + +`wpa_supplicant`: 运行无线网络连接 + +`dhclient ${card}`: 动态分配ip + +#### 3.安装步骤 + +> 本节介绍`rtl8188eu`芯片的驱动安装方式 + +##### 3.1 检测并调整本机时间 + +```bash +#检测本机时间是否正常 +> date +Tue May 3 00:34:41 CST 2022 +#不正常,则进行校准 +> date -s "2022/05/03 00:34:25" +#将当前系统时间写入到CMOS中 +> clock -w +``` + +##### 3.2 查看系统是否识别wifi芯片 + +```bash +#将无线网卡插入USB接口 +#查看系统是否识别芯片 +> lsusb +Bus 002 Device 002: ID 0bda:8179 Realtek Semiconductor Corp. RTL8188EUS 802.11n Wireless Network Adapter +``` + +##### 3.3 编译并安装驱动 + +```bash +#github: https://github.com/lwfinger/rtl8188eu +#进行编译,安装 +> make all +> make install +# 编译的时候若出现 +make: *** /lib/modules/3.10.0-1160.45.1.el7.x86_64/build/: No such file or directory. Stop. +#则将已有内核建立软连接 +> ln -s /usr/src/kernels/3.10.0-1127.el7.x86_64 /lib/modules/3.10.0-1160.45.1.el7.x86_64/build/ + +#有些驱动make install会直接安装成功,无需再次操作 +#可先查看驱动是否安装成功,成功则跳过后续步骤 +> lsmod | grep 8188 + +#将编译生成的ko文件拷贝到[内核路径],并在[内核路径]执行如下命令 +#该命令会将驱动加载到modules.dep、modules.dep.bin等文件 +> depmod -a +#系统载入驱动模块 +> modprobe 8188eu +#再次查看驱动是否安装成功 +> lsmod | grep 8188 +``` + +##### 3.4 激活网卡 + +```bash +#查看网卡情况 +> ifconfig -a +wlp0s29f7u1: flags=4163 mtu 1500 + ether 00:13:ef:f8:41:4c txqueuelen 1000 (Ethernet) + RX packets 90483 bytes 82632937 (78.8 MiB) + RX errors 0 dropped 3011 overruns 0 frame 0 + TX packets 30298 bytes 7919663 (7.5 MiB) + TX errors 0 dropped 79 overruns 0 carrier 0 collisions 0 +#激活网卡 +> ifconfig wlp0s29f7u1 up + +#需要配置开机自动加载,系统重启后,网卡才会被识别 +#创建 ifcfg-wlp0s29f7u1 文件 +> vim /etc/sysconfig/network-scripts/ifcfg-wlp0s29f7u1 +DEVICE=wlp0s29f7u1 +BOOTPROTO=static +ONBOOT=yes +NAME=wlp0s29f7u1 +``` + +##### 3.5 扫描网络热点并设置 + +```bash +#扫描无线热点 +#若能扫描成功,则说明网卡驱动正常 +> iwlist wlp0s29f7u1 scan +#设置无线热点账号及密码 +> wpa_passphrase ${wifi_ssid} > /etc/wpa_supplicant/wpa_supplicant.conf + #stdin + ${wifi_password} +#运行无线网络连接 +> wpa_supplicant -D wext -B -i wlp0s29f7u1 -c /etc/wpa_supplicant/wpa_supplicant.conf +rfkill: Cannot get wiphy information +ioctl[SIOCSIWAP]: Operation not permitted 出现这个没影响,实际已经连接成功 + +#分配ip及路由 +#可采用静态和动态两种方案 +#方案1:静态方案(推荐) +#固定ip +> ifconfig wlp0s29f7u1 192.168.1.120 +#设置路由 +> route add default gw 192.168.1.1 dev wlp0s29f7u1 + +#方案2:动态方案 +#一键分配动态ip及路由 (此方案发现通信性能不稳定) +> dhclient wlp0s29f7u1 +``` + +##### 3.6 优化ssh远程连接 + +由于客户端建立ssh远程连接时,可能出现非常卡顿的情况,优化sshd配置如下: + +```bash +> vim /etc/ssh/sshd_config +#原因一:SSH服务器默认开启了DNS的查询功能:UseDNS=yes +#当UseDNS选项开启时,客户端试图登录SSH服务器,服务器端先根据客户端的IP地址进行DNS PTR +#反向查询出客户端的主机名,然后根据查询出的客户端主机名进行DNS正向A记录查询,验证与其原始IP地址是否一致。 +#关闭DNS校验 +UseDNS=no + +#原因二:若SSH服务器开启了GSSAPI登录验证模式:GSSAPIAuthentication=yes +#GSSAPI是公共安全事务应用程序接口(GSS-API),仅用于SSH-2. +#若服务器开启了该验证机制,但客户端并未使用该身份验证机制,则会导致验证过程出现延迟 +#关闭GSSAPI验证机制 +GSSAPIAuthentication=no + + +#重启sshd服务 +> systemctl restart sshd +``` + +##### 3.7 开机自动建立连接 + +```bash +#可在rc.local配置如下命令,或自行shell实现开机自动联网需求 +#运行无线网络连接 +> wpa_supplicant -D wext -B -i wlp0s29f7u1 -c /etc/wpa_supplicant/wpa_supplicant.conf +#分配ip +> ifconfig wlp0s29f7u1 192.168.1.120 +#设置路由 +> route add default gw 192.168.1.1 dev wlp0s29f7u1 +``` diff --git a/shell/batch_nginx/bath.sh b/shell/batch_nginx/bath.sh new file mode 100644 index 0000000..165d76e --- /dev/null +++ b/shell/batch_nginx/bath.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# +# date: 2020.1.13 +# usage: bash deploy.sh 10.216.2.30 +# +# still need passord in scp & ssh command +# can optimize by expect command +################config######################### + +onlineIP=( +10.225.2.114 +10.225.1.84 +10.225.3.77 +10.225.3.168 +10.225.0.72 +10.225.3.236 +10.225.2.112 +) + +for ip in ${onlineIP[@]} +do +{ + echo "${ip} begin execute..." + echo "bash deploy.sh ${ip}" + bash deploy.sh ${ip} +} & +done + +wait + +echo '全部执行结束' diff --git a/shell/batch_nginx/deploy.sh b/shell/batch_nginx/deploy.sh new file mode 100644 index 0000000..2a9adee --- /dev/null +++ b/shell/batch_nginx/deploy.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# +# date: 2020.1.13 +# usage: bash deploy.sh 10.216.2.30 +# +# still need passord in scp & ssh command +# can optimize by expect command +################config######################### +remote_ip=$1 +user="yushaolong" +pwd="yourpassword" + +######################################### + +sudoroot="echo ${pwd} | sudo -S " + +ssh ${user}@${remote_ip} >./remote.${remote_ip}.log 2>&1 << EOF + +${sudoroot} mkdir -p /data/nginx/logs +${sudoroot} chmod 777 /data/nginx/logs +${sudoroot} mkdir -p /data/log/nginx +${sudoroot} chmod 777 /data/log/nginx + + +#2.start +${sudoroot} /usr/local/nginx/sbin/nginx + +echo 'nginx start success' +exit +EOF +echo "remote done success!" diff --git a/shell/tcp_opt.sh b/shell/tcp_opt.sh new file mode 100644 index 0000000..f33403d --- /dev/null +++ b/shell/tcp_opt.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# +# 调优linux tcp参数 +# +# 2020.01.15 +# +# yushaolong@360.cn +# +# -------------------------------配置 开始--------------------------------------------- + +#添加目标机器的ip列表 +onlineIP=( +10.21.2.1 +10.21.2.2 +10.21.2.3 +) + + +#修改执行账户和密码 +USER='yourname' +PWD='yourpassword' + + +#目标文件及日志名称 +EXE_FILE='linux_tcp.sh' +LOCAL_LOG='local_expect.log' +REMOTE_EXE_LOG='remote_linux_tcp.log' +sudoroot="echo '${PWD}' | sudo -S " + +#生成目标文件 +cat > ${EXE_FILE} <> /etc/sysctl.conf +${sudoroot} echo 'net.ipv4.tcp_tw_reuse = 1' >> /etc/sysctl.conf +${sudoroot} echo 'net.ipv4.tcp_tw_recycle = 1' >> /etc/sysctl.conf +${sudoroot} echo 'net.ipv4.tcp_fin_timeout = 30' >> /etc/sysctl.conf +${sudoroot} echo 'net.ipv4.tcp_max_tw_buckets = 15000' >> /etc/sysctl.conf + +#3.运行生效 +${sudoroot} /sbin/sysctl -p + +STD + +# -------------------------------配置 结束--------------------------------------------- + + +#对线上机操作 +CMD_PROMPT='~]$' + +function linux_expect() +{ + local _command=$1 + local _pwd=$2 + local _send_bash=$3 + local _exit_bash=$4 + { + expect -c " + set timeout 5 + spawn ${_command} + expect { + -re \"Are you sure you want to continue connecting(yes/no)?\" { + send \"yes\n\" + exp_continue + } + \"*password:\" { + send \"${_pwd}\n\" + exp_continue + } + \"*${CMD_PROMPT}*\" { + send \"${_send_bash}\n\" + } + } + expect \"*${CMD_PROMPT}*\" { + send \"${_exit_bash}\n\" + } + expect eof + " + } >> ${LOCAL_LOG} 2>&1 +} + + + + +for ip in ${onlineIP[@]} +do +{ + echo "${ip} begin execute..." + #scp目标文件到远程主机 + linux_expect "scp ${EXE_FILE} ${USER}@${ip}:~/" ${PWD} "exit" + #修改目标文件权限 + linux_expect "ssh ${USER}@${ip}" ${PWD} "echo '${PWD}' | sudo -S chmod +x ~/${EXE_FILE}" "exit" + #执行目标文件 + linux_expect "ssh ${USER}@${ip}" ${PWD} "echo '${PWD}' | sudo -S bash ~/${EXE_FILE} > ~/${REMOTE_EXE_LOG}" "exit" +} & +done + +wait + +echo '全部执行结束'