-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlocal-search.xml
334 lines (159 loc) · 380 KB
/
local-search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>网络部分-epoll分析</title>
<link href="/2024/11/29/%E7%BD%91%E7%BB%9CIO/"/>
<url>/2024/11/29/%E7%BD%91%E7%BB%9CIO/</url>
<content type="html"><![CDATA[<h2 id="1、内核如何接收数据"><a href="#1、内核如何接收数据" class="headerlink" title="1、内核如何接收数据"></a>1、内核如何接收数据</h2><p>不同主机通过网卡进行数据的交互,网卡将电磁波转换为模拟信号,再转换为数字信号,再由OSI模型传到应用层,变成人可以识别的数据。信号转换属于通信相关的知识,所以从接收到数字信号开始分析数据的流转。</p><ol><li>首先当数据帧从网线到达网卡上的时候,第一站是网卡的接收队列。网卡在分配给自己的RingBuffer中寻找可用的内存位置,找到后DMA引擎会把数据DMA到网卡之前关联的内存里,这个时候CPU都是无感的。当DMA操作完成以后,网卡会像CPU发起一个硬中断,通知CPU有数据到达。</li><li>Linux在硬中断里只完成简单必要的工作,剩下的大部分的处理都是转交给软中断的,硬中断处理过程真的是非常短。只是记录了一个寄存器,修改了一下下CPU的poll_list,然后发出个软中断。</li><li>软中断和硬中断中调用了同一个函数<code>local_softirq_pending</code>。使用方式不同的是硬中断位置是为了写入标记,这里仅仅只是读取这个标记。</li><li>把数据帧从RingBuffer上取下来,数据包将被送到协议栈中,ip -> tcp/udp,tcpdump就是在这里获取数据包</li><li>对应的协议栈将数据送往对应的socket,socket通知对应的进程</li></ol><h2 id="2、socket的概念"><a href="#2、socket的概念" class="headerlink" title="2、socket的概念"></a>2、socket的概念</h2><h3 id="2-1、通过socket唤醒各个进程"><a href="#2-1、通过socket唤醒各个进程" class="headerlink" title="2.1、通过socket唤醒各个进程"></a>2.1、通过socket唤醒各个进程</h3><p>用户进程可以通过socket接口与内核进行数据的交互,当一个进程想要listen一个端口时,首先需要创建一个socket绑定这个端口,当这个端口收到数据时,内核先将数据送往对应的协议栈,协议栈主要做2个事情</p><ul><li>保存数据到socket的接收缓冲队列</li><li>唤醒队列上的进程</li></ul><p>拿多进程举例子,如果master监听了某端口后,会创建对应的socket,后续fork时,子进程也共享这个打开的socket,也会”监听”这个socket(当某个进程调用 accept() 时,内核会动态地将该进程注册到 socket 的等待队列中),这就导致socket的等待队列会有多个work进程阻塞在这里,也就是说socket的等待队列存在多个进程。</p><p> 多进程共享一个socket,但是进程各自有epoll</p><p>在唤醒进程时,如果唤醒所有的进程,就会引发惊群效应。有了epoll后,每个进程都会有自己的epoll,相当于会唤醒所有的epoll ,唤醒本质就是调用epoll注册的回调函数。</p><h3 id="2-2、通过epoll唤醒各个进程"><a href="#2-2、通过epoll唤醒各个进程" class="headerlink" title="2.2、通过epoll唤醒各个进程"></a>2.2、通过epoll唤醒各个进程</h3><p>根据socket等待队列中的元素,找到对应的epoll和epitem,这个回调函数第一时间会把epitem放到epoll对象的就绪链表里面,后续epoll_wait就会从就绪链表读取事件进行处理。</p><p>同样唤醒时可以选择唤醒一个进程还是多个</p><p> 多进程共享一个epoll</p><h2 id="3、惊群效应"><a href="#3、惊群效应" class="headerlink" title="3、惊群效应"></a>3、惊群效应</h2><p>当讨论到惊群效应,其实要分层次讨论,因为socket和epoll都会有惊群效应。简单来说先fork再epoll_create(),socket的等待队列会存在多个””进程”; 先epoll_create()再fork,epoll的等待队列会有多个进程;</p><h3 id="3-1、socket的惊群效应"><a href="#3-1、socket的惊群效应" class="headerlink" title="3.1、socket的惊群效应"></a>3.1、socket的惊群效应</h3><p>多个进程共享一个socket,即主进程create、bind、listen,然后fork子进程后,多个进程共享一个socket,进行accept的场景,这时候socket的等待队列会存在多个进程。</p><p>当使用epoll时,那就意味着socket的等待队列存在多个epoll。比如nginx的每个work都会有自己的epoll,会把这个socket注册到epoll,相应的注册回调到socket的等待队列,相当于把本进程注册到socket。</p><p>当数据被送到socket时,socket会”唤醒”等待队列的各个进程(其实是调用epoll注册的回调函数),这时候如果唤醒所有的进程,就会引发惊群效应,因为只有一个进程会accept成功。</p><p>解决办法</p><ol><li>使用WQ_FLAG_EXCLUSIVE,在唤醒进程时,不会唤醒所有的进程,只会唤醒一个进程,但是解决不了epoll的场景。( Linux 2.6 版本中引入)</li><li>使用SO_REUSEPORT,每个进程都有自己的socket,大家不共享socket,由内核负载socket。 (Linux 3.9 版本中引入)</li><li>使用锁,在应用层解决竞争关系,只有拿到锁的进程才能accept,nginx早期就是这么做的</li></ol><p>对于epoll,用户层可以调用的EPOLLEXCLUSIVE,实际使用的就是WQ_FLAG_EXCLUSIVE,使用EPOLLEXCLUSIVE ,添加事件时,epoll 会将对应的 epitem 节点标记为“独占模式” ,带有 EPOLLEXCLUSIVE 的监听者会被加入独占等待队列中,而非普通等待队列。如果有多个监听者,只会唤醒等待队列中第一个处于 EPOLLEXCLUSIVE 模式的监听者。如果没有 EPOLLEXCLUSIVE 模式监听者,唤醒其他普通监听者。(Linux 4.5 版本中引入)</p><h3 id="3-2、epoll的惊群效应"><a href="#3-2、epoll的惊群效应" class="headerlink" title="3.2、epoll的惊群效应"></a>3.2、epoll的惊群效应</h3><p>多进程共享一个epoll,即多个进程共享一个epoll的对象。一个主进程先epoll_create(),然后再fork() 创建多个进程。其实这种模式常见于多线程(其实也不常见吧,应该不会有人会这么设计吧?),使用pthread_create()创建线程,本质上和fork没区别,实际上进程和进程也没区别,都会调用到 kernel_clone(),区别在于传入的参数不一样,这个函数会根据参数的不同,执行不同的逻辑,结果就是子进程会不会与父进程共享地址等等。</p><p>各个进程调用epoll_wait时,会把自己注册到epoll的等待队列,这会导致epoll的等待队列存在多个进程。</p><p>当socket执行到epoll的回调函数时,epoll首先会把自己的epitem放到就绪链表,然后唤醒等待队列的进程,其实就是执行等待队列元素的回调函数。如果唤醒所有的进程,那就会引发惊群效应。</p><p>解决办法</p><ol><li><p>实际查看linux代码,epoll_wait会默认设置独占模式。用户调用 epoll_wait 进入阻塞状态,如果没有事件,就阻塞自己,把当前进程写入到epoll元素的等待队列中,并设置WQ_FLAG_EXCLUSIVE。那其实就意味着这种场景没有惊群效应。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-type">void</span><br>__add_wait_queue_exclusive(<span class="hljs-keyword">struct</span> wait_queue_head *wq_head, <span class="hljs-keyword">struct</span> wait_queue_entry *wq_entry)<br>{<br>wq_entry->flags |= WQ_FLAG_EXCLUSIVE; <span class="hljs-comment">//设置WQ_FLAG_EXCLUSIVE</span><br>__add_wait_queue(wq_head, wq_entry);<br>}<br></code></pre></td></tr></table></figure></li></ol><p>不过一般多线程的架构设计不会这么设计,一般会主线程负责accept,在创建新的socket连接后,交由work线程,work会把这个新的socket加到自己的epoll,然后处理后续的事件。</p><p>不过多进程共享一个epoll绝对是不好的设计。</p><h3 id="3-3、各个属性分析"><a href="#3-3、各个属性分析" class="headerlink" title="3.3、各个属性分析"></a>3.3、各个属性分析</h3><p>1、WQ_FLAG_EXCLUSIVE</p><ol><li>减少惊群效应:<code>WQ_FLAG_EXCLUSIVE</code> 主要用于减少多个进程或进程同时被唤醒的情况,即惊群效应。当多个进程或进程等待同一个socket上的事件时,一个新连接的到来会导致所有阻塞在该socket上的进程或进程都被唤醒,但最终只有一个能处理这个连接,其余的进程或进程会重新进入等待状态。</li><li>内核层面的优化:<code>WQ_FLAG_EXCLUSIVE</code> 通过内核排他性唤醒机制,确保一次只唤醒一个等待队列中的进程,从而减少不必要的上下文切换和性能损耗。</li></ol><p>2、SO_REUSEPORT</p><ol><li>端口复用:<code>SO_REUSEPORT</code> 允许多个进程或进程绑定到同一端口上,每个进程或进程独立处理收到的数据。这在传统的socket编程中是不允许的,因为一个端口只能被一个进程绑定。</li><li>负载均衡:<code>SO_REUSEPORT</code> 不仅允许多个进程绑定到同一端口,还能在内核层面实现负载均衡,将新连接均匀分配给不同的进程或进程,从而提高多核系统的并行处理能力和整体性能。</li></ol><p>区别和联系</p><ul><li>作用层面:<code>WQ_FLAG_EXCLUSIVE</code> 主要是在内核层面减少不必要的进程唤醒,而 <code>SO_REUSEPORT</code> 是在应用层面允许多个进程或进程共享同一个端口,并在内核层面实现负载均衡。</li><li>应用场景:<code>WQ_FLAG_EXCLUSIVE</code> 更适用于单个进程内部的进程间协作,减少进程间的唤醒竞争;而 <code>SO_REUSEPORT</code> 更适用于多个进程间共享端口资源,提高系统的并发处理能力。</li><li>性能优化:<code>WQ_FLAG_EXCLUSIVE</code> 通过减少不必要的进程唤醒来优化性能;<code>SO_REUSEPORT</code> 通过负载均衡和多进程/进程处理来提高性能。</li></ul><h3 id="3-4、源码分析"><a href="#3-4、源码分析" class="headerlink" title="3.4、源码分析"></a>3.4、源码分析</h3><p>1、对于socket</p><p>到达协议栈后,最终会调用__wake_up_sync_key唤醒进程</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">void</span> __wake_up_sync_key(<span class="hljs-keyword">struct</span> wait_queue_head *wq_head, <span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> mode,<br><span class="hljs-type">void</span> *key)<br>{<br> <span class="hljs-keyword">if</span> (unlikely(!wq_head))<br> <span class="hljs-keyword">return</span>;<br> <br> __wake_up_common_lock(wq_head, mode, <span class="hljs-number">1</span>, WF_SYNC, key); <span class="hljs-comment">//这里的参数传了1</span><br>}<br></code></pre></td></tr></table></figure><p>调用到wake_up_common_lock -> __wake_up_common,其中nr_exclusive传了1</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">int</span> __wake_up_common(<span class="hljs-keyword">struct</span> wait_queue_head *wq_head, <span class="hljs-type">unsigned</span> <span class="hljs-type">int</span> mode,<br><span class="hljs-type">int</span> nr_exclusive, <span class="hljs-type">int</span> wake_flags, <span class="hljs-type">void</span> *key)<br>{<br><span class="hljs-type">wait_queue_entry_t</span> *curr, *next;<br>lockdep_assert_held(&wq_head->lock);<br>curr = list_first_entry(&wq_head->head, <span class="hljs-type">wait_queue_entry_t</span>, entry);<br><br><span class="hljs-comment">// 如果列表为空,则直接返回未使用的独占型进程唤醒名额</span><br><span class="hljs-keyword">if</span> (&curr->entry == &wq_head->head)<br><span class="hljs-keyword">return</span> nr_exclusive;<br><br><span class="hljs-comment">// 安全地遍历等待队列</span><br>list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {<br><span class="hljs-type">unsigned</span> flags = curr->flags;<br><span class="hljs-type">int</span> ret;<br><span class="hljs-comment">// 调用当前队列项的唤醒函数</span><br>ret = curr->func(curr, mode, wake_flags, key);<br><span class="hljs-comment">// 如果唤醒函数返回负值,则停止遍历</span><br><span class="hljs-keyword">if</span> (ret < <span class="hljs-number">0</span>)<br><span class="hljs-keyword">break</span>;<br><span class="hljs-comment">// 如果唤醒成功且当前队列项是独占型的,则减少剩余的独占型进程唤醒名额</span><br><span class="hljs-keyword">if</span> (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) <span class="hljs-comment">//关键在这里</span><br><span class="hljs-keyword">break</span>;<br>}<br><span class="hljs-comment">// 返回未使用的独占型进程唤醒名额</span><br><span class="hljs-keyword">return</span> nr_exclusive;<br>}<br></code></pre></td></tr></table></figure><p>重点在于</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">if</span> (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)<br><span class="hljs-keyword">break</span>;<br></code></pre></td></tr></table></figure><ul><li><strong><code>ret</code></strong>: 唤醒操作是否成功。如果为 <code>true</code>,表示有进程被成功唤醒。</li><li><strong><code>flags & WQ_FLAG_EXCLUSIVE</code></strong>: 检查当前队列项是否为独占型。<code>WQ_FLAG_EXCLUSIVE</code> 是一个标志位,表示该队列项是独占型的。</li><li><strong><code>!--nr_exclusive</code></strong>: 减少剩余的独占型进程唤醒名额,并检查是否已经用完所有名额。<code>--nr_exclusive</code> 先将 <code>nr_exclusive</code> 减 1,然后取其值。如果减 1 后 <code>nr_exclusive</code> 变为 0,则 <code>!</code> 运算符将其转换为 <code>true</code>。</li></ul><p>代码逻辑分析</p><ol><li>唤醒成功:<code>ret</code> 为 <code>true</code>,表示有进程被成功唤醒。</li><li>独占型队列项:<code>flags & WQ_FLAG_EXCLUSIVE</code> 为 <code>true</code>,表示当前队列项是独占型的。</li><li>减少独占型唤醒名额:<code>--nr_exclusive</code> 将 <code>nr_exclusive</code> 减 1。</li><li>检查名额是否用完:如果 <code>nr_exclusive</code> 减 1 后变为 0,则 <code>!</code> 运算符将其转换为 <code>true</code>,执行 <code>break</code> 语句,跳出循环。</li></ol><p>通常nr_exclusive为1,也就是唤醒独占型的1个进程。但是会不会发生下面的场景呢?非独占型的进程在前面,独占型的进程在后面</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">/*假设队列为:A(非独占) → B(独占) → C(独占) → D(非独占)</span><br><span class="hljs-comment">任务 A(非独占):</span><br><span class="hljs-comment"></span><br><span class="hljs-comment">被唤醒(ret = 1),继续遍历。</span><br><span class="hljs-comment">任务 B(独占):</span><br><span class="hljs-comment"></span><br><span class="hljs-comment">被唤醒(ret = 1),nr_exclusive--。</span><br><span class="hljs-comment">此时 nr_exclusive = 0,触发 break。</span><br><span class="hljs-comment">循环终止,任务 C 和任务 D 不会被处理。*/</span><br></code></pre></td></tr></table></figure><p>但是实际linux在添加进程时,会优先把独占型的进程添加到头部,新的独占型总会加到独占型的最后一个,如果没有他就会第一个,例如</p><ul><li>A(非独占)B(独占)C(独占)D(非独占)</li></ul><p>A</p><p>B->A</p><p>B->C->A</p><p>B->C->A->D</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-type">void</span> __add_wait_queue(<span class="hljs-keyword">struct</span> wait_queue_head *wq_head, <span class="hljs-keyword">struct</span> wait_queue_entry *wq_entry)<br>{<br> <span class="hljs-comment">// 获取等待队列头部的列表头指针。</span><br> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">list_head</span> *<span class="hljs-title">head</span> =</span> &wq_head->head;<br> <span class="hljs-comment">// 定义一个等待队列项指针,用于遍历等待队列。</span><br> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">wait_queue_entry</span> *<span class="hljs-title">wq</span>;</span><br><br> <span class="hljs-comment">// 遍历等待队列头部中的所有等待队列项。</span><br> list_for_each_entry(wq, &wq_head->head, entry) {<br> <span class="hljs-comment">// 检查当前等待队列项是否具有优先级标志。</span><br> <span class="hljs-keyword">if</span> (!(wq->flags & WQ_FLAG_PRIORITY))<br> <span class="hljs-keyword">break</span>;<br> <span class="hljs-comment">// 如果当前项有优先级标志,则更新列表头指针为当前项的列表入口。</span><br> head = &wq->entry;<br> }<br> <span class="hljs-comment">// 将新的等待队列项添加到找到的位置之前。</span><br> list_add(&wq_entry->entry, head);<br>}<br></code></pre></td></tr></table></figure><p>所以如果有独占型的进程,那确实不会唤醒其他非独占型的进程,WQ_FLAG_PRIORITY和nr_exclusive决定了最终唤醒的结果,让我们分析下4种组合的情况</p><hr><table><thead><tr><th><strong>场景</strong></th><th><strong>队列中是否有 <code>WQ_FLAG_EXCLUSIVE</code></strong></th><th><strong><code>nr_exclusive</code></strong></th><th><strong>结果</strong></th><th><strong>返回值</strong></th></tr></thead><tbody><tr><td><strong>1</strong> 无独占任务</td><td>否</td><td>1</td><td>所有非独占任务被唤醒;<code>nr_exclusive</code> 不减少</td><td>1</td></tr><tr><td><strong>2</strong> 无独占任务</td><td>否</td><td>0</td><td>所有非独占任务被唤醒;<code>nr_exclusive</code> 不减少</td><td>0</td></tr><tr><td><strong>3</strong> 有独占任务</td><td>是</td><td>1</td><td>唤醒一个独占任务后退出;</td><td>0</td></tr><tr><td><strong>4</strong> 有独占任务</td><td>是</td><td>0</td><td>所有任务(独占和非独占)都被唤醒;<code>nr_exclusive</code> 递减到负数</td><td>负数(最终值)</td></tr></tbody></table><hr><p>其实socket的处理过程中,nr_exclusive一直是1,所以到底唤醒一个进程还是多个,由用户层决定,毕竟WQ_FLAG_EXCLUSIVE是可以设置的。</p><p>2、对于epoll</p><p>epoll在唤醒它的等待队列中的元素时,依次调用了 wake_up_locked() -> __wake_up_locked -> wake_up_common</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> wake_up_locked(x)__wake_up_locked((x), TASK_NORMAL, 1)</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> wake_up_all_locked(x)__wake_up_locked((x), TASK_NORMAL, 0)</span><br></code></pre></td></tr></table></figure><p>wake_up_locked() 和 wake_up_all_locked() 都是用于唤醒等待队列中的进程的宏,最后都调用了 wake_up_common,原理也如上述所示。</p><p>但是因为epoll_wait,将进程设置到epoll的等待链表时,会默认设置WQ_FLAG_EXCLUSIVE(要明白,这里区别于socket的等待队列,因为socket设置时并不会默认设置WQ_FLAG_EXCLUSIVE),且nr_exclusive传了1,按照组合的情况,只会唤醒一个独占进程。</p><h2 id="4、epoll实现原理"><a href="#4、epoll实现原理" class="headerlink" title="4、epoll实现原理"></a>4、epoll实现原理</h2><p>epoll主要涉及3个接口</p><ul><li>epoll_create:创建一个 epoll 对象</li><li>epoll_ctl:向 epoll 对象中添加要管理的连接</li><li>epoll_wait:等待其管理的连接上的 IO 事件</li></ul><h3 id="4-1、epoll-create"><a href="#4-1、epoll-create" class="headerlink" title="4.1、epoll_create"></a>4.1、epoll_create</h3><p>在用户进程调用 epoll_create 时,内核会创建一个 struct eventpoll 的内核对象</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">eventpoll</span> {</span><br><br> <span class="hljs-comment">//sys_epoll_wait用到的等待队列</span><br> <span class="hljs-type">wait_queue_head_t</span> wq;<br><br> <span class="hljs-comment">//接收就绪的描述符都会放到这里</span><br> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">list_head</span> <span class="hljs-title">rdllist</span>;</span><br><br> <span class="hljs-comment">//每个epoll对象中都有一颗红黑树</span><br> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">rb_root</span> <span class="hljs-title">rbr</span>;</span><br><br> ......<br>}<br></code></pre></td></tr></table></figure><p>eventpoll 这个结构体中的几个成员的含义如下:</p><ul><li><strong>wq:</strong> 等待队列链表。软中断数据就绪的时候会通过 wq 来找到阻塞在 epoll 对象上的用户进程。</li><li><strong>rbr:</strong> 一棵红黑树。为了支持对海量连接的高效查找、插入和删除,eventpoll 内部使用了一棵红黑树。通过这棵树来管理用户进程下添加进来的所有 socket 连接。</li><li><strong>rdllist:</strong> 就绪的描述符的链表。当有的连接就绪的时候,内核会把就绪的连接放到 rdllist 链表里。这样应用进程只需要判断链表就能找出就绪进程,而不用去遍历整棵树。</li></ul><h3 id="4-2、epoll-ctl"><a href="#4-2、epoll-ctl" class="headerlink" title="4.2、epoll_ctl"></a>4.2、epoll_ctl</h3><p>使用 epoll_ctl 注册每一个 socket 的时候,内核会做如下三件事情</p><ul><li>1.分配一个红黑树节点对象 epitem,</li><li>2.添加等待事件到 socket 的等待队列中,其回调函数是 ep_poll_callback</li><li>3.将 epitem 插入到 epoll 对象的红黑树里</li></ul><p>对于每一个 socket,调用 epoll_ctl 的时候,都会为之分配一个 epitem。该结构的主要数据如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">//file: fs/eventpoll.c</span><br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">epitem</span> {</span><br><br> <span class="hljs-comment">//红黑树节点</span><br> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">rb_node</span> <span class="hljs-title">rbn</span>;</span><br><br> <span class="hljs-comment">//socket文件描述符信息</span><br> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">epoll_filefd</span> <span class="hljs-title">ffd</span>;</span><br><br> <span class="hljs-comment">//所归属的 eventpoll 对象</span><br> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">eventpoll</span> *<span class="hljs-title">ep</span>;</span><br><br> <span class="hljs-comment">//等待队列</span><br> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">list_head</span> <span class="hljs-title">pwqlist</span>;</span><br>}<br></code></pre></td></tr></table></figure><p>在创建 epitem 并初始化之后,ep_insert 中第二件事情就是设置 socket 对象上的等待任务队列。并把函数 fs/eventpoll.c 文件下的 ep_poll_callback 设置为数据就绪时候的回调函数。</p><p>在这个函数里它获取了 sock 对象下的等待队列列表头 wait_queue_head_t,待会等待队列项就插入这里。这里稍微注意下,是 socket 的等待队列,不是 epoll 对象的</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">//file:include/linux/wait.h</span><br><span class="hljs-type">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-type">void</span> <span class="hljs-title function_">init_waitqueue_func_entry</span><span class="hljs-params">(</span><br><span class="hljs-params"> <span class="hljs-type">wait_queue_t</span> *q, <span class="hljs-type">wait_queue_func_t</span> func)</span><br>{<br> q->flags = <span class="hljs-number">0</span>;<br> q->private = <span class="hljs-literal">NULL</span>;<br><br> <span class="hljs-comment">//ep_poll_callback 注册到 wait_queue_t对象上</span><br> <span class="hljs-comment">//有数据到达的时候调用 q->func</span><br> q->func = func; <br>}<br></code></pre></td></tr></table></figure><p>分配完 epitem 对象后,紧接着并把它插入到红黑树中</p><h3 id="4-3、epoll-wait"><a href="#4-3、epoll-wait" class="headerlink" title="4.3、epoll_wait"></a>4.3、epoll_wait</h3><p>当它被调用时它观察 eventpoll->rdllist 链表里有没有数据即可。有数据就返回,没有数据就创建一个等待队列项,将其添加到 eventpoll 的等待队列上,然后把自己阻塞掉。</p><p>再回顾一下,这里添加到 eventpoll 的等待队列上时,会附带WQ_FLAG_EXCLUSIVE属性。</p><h3 id="4-4、接收数据"><a href="#4-4、接收数据" class="headerlink" title="4.4、接收数据"></a>4.4、接收数据</h3><p>当网卡的数据交到tcp协议栈时,协议栈会找到对应的socket,然后回调socket等待队列中的”epoll”们,找到了 socket 等待队列项里注册的函数 ep_poll_callback,软中断接着就会调用它。首先把自己的 epitem 添加到 epoll 的就绪队列中。接着它又会查看 eventpoll 对象上的等待队列里是否有等待项(epoll_wait 执行的时候会设置)。</p><p>在 __wake_up_common里, 调用 curr->func。这里的 func 是在 epoll_wait 是传入的 default_wake_function 函数。在default_wake_function 中找到等待队列项里的进程描述符,然后唤醒之。将epoll_wait进程推入可运行队列,等待内核重新调度进程。然后epoll_wait对应的这个进程重新运行后,就从 schedule 恢复</p><p>当进程醒来后,继续从 epoll_wait 时暂停的代码继续执行。把 rdlist 中就绪的事件返回给用户进程,用户进程对其进行处理。</p><hr><p>我觉得epoll的关键就在于一个进程在运行时可以处理多个socket,而不是每次处理一个socket,然后阻塞睡眠,然后再唤醒,这会导致大量的进程切换。所以快就快在减少了进程切换带来的损耗。</p><p>当然如果没有事件触发,epoll就会阻塞自己,可以说epoll本身是阻塞的,但是socket可以设置为非阻塞,即read 和 write都是非阻塞的。</p><p>epoll 不负责真正的数据读写,它只是告诉用户程序哪些 <code>socket</code> 可以进行读写操作。在真正的异步 I/O 模型(如 <code>io_uring</code> 或 POSIX AIO)中,应用程序发起 I/O 操作后立即返回,由内核或驱动完成 I/O 操作,并通过回调或信号通知操作结果。所以epoll 的工作方式仍然需要应用程序显式调用处理函数,因此是同步的。</p><p>整体属于同步非阻塞。</p><hr><h2 id="5、nginx如何支持IO复用"><a href="#5、nginx如何支持IO复用" class="headerlink" title="5、nginx如何支持IO复用"></a>5、nginx如何支持IO复用</h2><p>nginx本质上属于同步非阻塞,当socket触发事件时,Nginx 自己去处理事件数据,操作和完成整个流程,而不是像真正的异步模型那样由底层系统自动完成。</p><h3 id="5-1、nginx怎么选择IO复用"><a href="#5-1、nginx怎么选择IO复用" class="headerlink" title="5.1、nginx怎么选择IO复用"></a>5.1、nginx怎么选择IO复用</h3><p>nginx在初始化事件模块时,根据系统支持的事件模型(如epoll、devpoll、kqueue、select)选择一个合适的事件模块,支持哪些模型由编译期决定,比如linux支持epoll,windows支持kqueue,因此编译的宏也是不一样的。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">if</span> (NGX_HAVE_EPOLL) && !(NGX_TEST_BUILD_EPOLL)</span><br><br> fd = epoll_create(<span class="hljs-number">100</span>);<br><br> <span class="hljs-keyword">if</span> (fd != <span class="hljs-number">-1</span>) {<br> (<span class="hljs-type">void</span>) close(fd);<br> module = &ngx_epoll_module;<br><br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (ngx_errno != NGX_ENOSYS) {<br> module = &ngx_epoll_module;<br> }<br><br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br><br>...........................<br> <br> event_module = module->ctx;<br></code></pre></td></tr></table></figure><p>定义ngx_epoll_module_ctx</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> {</span><br> <span class="hljs-type">ngx_str_t</span> *name;<br><br> <span class="hljs-type">void</span> *(*create_conf)(<span class="hljs-type">ngx_cycle_t</span> *cycle);<br> <span class="hljs-type">char</span> *(*init_conf)(<span class="hljs-type">ngx_cycle_t</span> *cycle, <span class="hljs-type">void</span> *conf);<br><br> <span class="hljs-type">ngx_event_actions_t</span> actions; <span class="hljs-comment">//定义的方法集合</span><br>} <span class="hljs-type">ngx_event_module_t</span>;<br><br><span class="hljs-type">static</span> <span class="hljs-type">ngx_event_module_t</span> ngx_epoll_module_ctx = {<br> &epoll_name,<br> ngx_epoll_create_conf, <span class="hljs-comment">/* create configuration */</span><br> ngx_epoll_init_conf, <span class="hljs-comment">/* init configuration */</span><br><br> {<br> ngx_epoll_add_event, <span class="hljs-comment">/* add an event */</span><br> ngx_epoll_del_event, <span class="hljs-comment">/* delete an event */</span><br> ngx_epoll_add_event, <span class="hljs-comment">/* enable an event */</span><br> ngx_epoll_del_event, <span class="hljs-comment">/* disable an event */</span><br> ngx_epoll_add_connection, <span class="hljs-comment">/* add an connection */</span><br> ngx_epoll_del_connection, <span class="hljs-comment">/* delete an connection */</span><br><span class="hljs-meta">#<span class="hljs-keyword">if</span> (NGX_HAVE_EVENTFD)</span><br> ngx_epoll_notify, <span class="hljs-comment">/* trigger a notify */</span><br><span class="hljs-meta">#<span class="hljs-keyword">else</span></span><br> <span class="hljs-literal">NULL</span>, <span class="hljs-comment">/* trigger a notify */</span><br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br> ngx_epoll_process_events, <span class="hljs-comment">/* process the events */</span><br> ngx_epoll_init, <span class="hljs-comment">/* init the events */</span><br> ngx_epoll_done, <span class="hljs-comment">/* done the events */</span><br> }<br>};<br></code></pre></td></tr></table></figure><p>epoll初始化时</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c">ngx_event_actions = ngx_epoll_module_ctx.actions;<br></code></pre></td></tr></table></figure><p>设置事件模块的回调为epoll的函数,如果使用的是poll,那将会是poll的方法</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> ngx_process_events ngx_event_actions.process_events</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> ngx_done_events ngx_event_actions.done</span><br><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> ngx_add_event ngx_event_actions.add</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> ngx_del_event ngx_event_actions.del</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> ngx_add_conn ngx_event_actions.add_conn</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> ngx_del_conn ngx_event_actions.del_conn</span><br><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> ngx_notify ngx_event_actions.notify</span><br></code></pre></td></tr></table></figure><h3 id="5-2、SO-REUSEPORT属性"><a href="#5-2、SO-REUSEPORT属性" class="headerlink" title="5.2、SO_REUSEPORT属性"></a>5.2、SO_REUSEPORT属性</h3><p>首先在进程初始化解析配置时,会判断一个端口是否开启了reuseport,如果开启了,套接字 <code>nls[n]</code> 的 <code>reuseport</code> 标志为真</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">if</span> (NGX_HAVE_REUSEPORT)</span><br> <span class="hljs-keyword">if</span> (nls[n].reuseport && !ls[i].reuseport) {<br> nls[n].add_reuseport = <span class="hljs-number">1</span>; <span class="hljs-comment">//</span><br> }<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br></code></pre></td></tr></table></figure><p>每个进程初始化event模块配置时</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">if</span> (NGX_HAVE_REUSEPORT)</span><br><br> <span class="hljs-comment">// 获取核心配置结构体</span><br> ccf = (<span class="hljs-type">ngx_core_conf_t</span> *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);<br><br> <span class="hljs-comment">// 如果不是测试配置且master进程存在,则进行端口复用处理</span><br> <span class="hljs-keyword">if</span> (!ngx_test_config && ccf->master) {<br><br> <span class="hljs-comment">// 获取监听套接字数组</span><br> ls = cycle->listening.elts;<br> <span class="hljs-comment">// 遍历所有监听套接字</span><br> <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i < cycle->listening.nelts; i++) {<br><br> <span class="hljs-comment">// 跳过未启用reuseport或worker不为0的监听套接字</span><br> <span class="hljs-keyword">if</span> (!ls[i].reuseport || ls[i].worker != <span class="hljs-number">0</span>) {<br> <span class="hljs-keyword">continue</span>;<br> }<br><br> <span class="hljs-comment">// 克隆监听套接字,如果克隆失败,则返回配置错误</span><br> <span class="hljs-keyword">if</span> (ngx_clone_listening(cycle, &ls[i]) != NGX_OK) {<br> <span class="hljs-keyword">return</span> NGX_CONF_ERROR;<br> }<br><br> <span class="hljs-comment">// 克隆操作可能更改cycle->listening.elts指针,因此重新获取指针</span><br> ls = cycle->listening.elts;<br> }<br> }<br><br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br></code></pre></td></tr></table></figure><p>关键是ngx_clone_listening:克隆当前监听套接字,为每个 <code>worker</code> 进程创建一个独立的监听实例,并设置其worker属性</p><ul><li>每个克隆的监听套接字会分配给一个 <code>worker</code>,并具有独立的监听队列。</li><li>这样每个进程能独立处理自己的客户端连接,避免锁争用,提高性能。</li></ul><p>每个进程初始化时,确保每个工作进程(worker)只保留属于自己的监听套接字,关闭那些不属于自己的套接字</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">if</span> (NGX_HAVE_REUSEPORT)</span><br> <span class="hljs-comment">// 检查当前监听套接字是否启用了reuseport选项并且不是当前工作进程的</span><br> <span class="hljs-keyword">if</span> (ls[i].reuseport && ls[i].worker != ngx_worker) {<br> <span class="hljs-comment">// 如果是,记录调试信息并关闭该套接字</span><br> ngx_log_debug2(NGX_LOG_DEBUG_CORE, cycle-><span class="hljs-built_in">log</span>, <span class="hljs-number">0</span>,<br> <span class="hljs-string">"closing unused fd:%d listening on %V"</span>,<br> ls[i].fd, &ls[i].addr_text);<br><br> <span class="hljs-comment">// 关闭套接字并检查是否成功</span><br> <span class="hljs-keyword">if</span> (ngx_close_socket(ls[i].fd) == <span class="hljs-number">-1</span>) {<br> <span class="hljs-comment">// 如果关闭失败,记录错误信息</span><br> ngx_log_error(NGX_LOG_EMERG, cycle-><span class="hljs-built_in">log</span>, ngx_socket_errno,<br> ngx_close_socket_n <span class="hljs-string">" %V failed"</span>,<br> &ls[i].addr_text);<br> }<br><br> <span class="hljs-comment">// 将套接字描述符设置为无效值</span><br> ls[i].fd = (<span class="hljs-type">ngx_socket_t</span>) <span class="hljs-number">-1</span>;<br><br> <span class="hljs-comment">// 继续处理下一个监听套接字</span><br> <span class="hljs-keyword">continue</span>;<br> }<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br></code></pre></td></tr></table></figure><p>最后看一下将epoll注册到socket内核中,ngx_add_event -> epoll_ctl</p><ul><li>开启了reuseport时,将本进程的epoll注册到socket</li><li>开启了EPOLLEXCLUSIVE时,将本进程的epoll独占式的注册到socket</li><li>如果什么都没开启,则将本进程的epoll注册到socket</li></ul><p>其中<br>c = rev->->data</p><p>c->fd就是本进程监听的socket,在上面已经设置过了</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// 如果定义了NGX_HAVE_REUSEPORT宏</span><br><span class="hljs-meta">#<span class="hljs-keyword">if</span> (NGX_HAVE_REUSEPORT)</span><br><br> <span class="hljs-comment">// 如果当前监听套接字启用了reuseport选项</span><br> <span class="hljs-keyword">if</span> (ls[i].reuseport) {<br> <span class="hljs-comment">// 尝试为读事件添加epoll事件,如果不成功则返回错误</span><br> <span class="hljs-keyword">if</span> (ngx_add_event(rev, NGX_READ_EVENT, <span class="hljs-number">0</span>) == NGX_ERROR) {<br> <span class="hljs-keyword">return</span> NGX_ERROR;<br> }<br><br> <span class="hljs-comment">// 继续处理下一个监听套接字</span><br> <span class="hljs-keyword">continue</span>;<br> }<br><br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br><br> <span class="hljs-comment">// 如果使用了accept mutex机制,则跳过添加epoll事件</span><br> <span class="hljs-keyword">if</span> (ngx_use_accept_mutex) {<br> <span class="hljs-keyword">continue</span>;<br> }<br><br><span class="hljs-comment">// 如果定义了NGX_HAVE_EPOLLEXCLUSIVE宏</span><br><span class="hljs-meta">#<span class="hljs-keyword">if</span> (NGX_HAVE_EPOLLEXCLUSIVE)</span><br><br> <span class="hljs-comment">// 如果使用了epoll事件模型且配置了多个工作进程</span><br> <span class="hljs-keyword">if</span> ((ngx_event_flags & NGX_USE_EPOLL_EVENT)<br> && ccf->worker_processes > <span class="hljs-number">1</span>)<br> {<br> <span class="hljs-comment">// 启用exclusive accept模式</span><br> ngx_use_exclusive_accept = <span class="hljs-number">1</span>;<br><br> <span class="hljs-comment">// 尝试以独占方式为读事件添加epoll事件,如果不成功则返回错误</span><br> <span class="hljs-keyword">if</span> (ngx_add_event(rev, NGX_READ_EVENT, NGX_EXCLUSIVE_EVENT)<br> == NGX_ERROR)<br> {<br> <span class="hljs-keyword">return</span> NGX_ERROR;<br> }<br><br> <span class="hljs-comment">// 继续处理下一个监听套接字</span><br> <span class="hljs-keyword">continue</span>;<br> }<br><br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br><br> <span class="hljs-comment">// 如果上述条件都不满足,尝试为读事件添加epoll事件,如果不成功则返回错误</span><br> <span class="hljs-keyword">if</span> (ngx_add_event(rev, NGX_READ_EVENT, <span class="hljs-number">0</span>) == NGX_ERROR) {<br> <span class="hljs-keyword">return</span> NGX_ERROR;<br> }<br></code></pre></td></tr></table></figure><p>最后再看下不使用reuseport,常规使用锁的情况</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// 检查是否需要使用接受互斥锁</span><br><span class="hljs-keyword">if</span> (ngx_use_accept_mutex) {<br> <span class="hljs-comment">// 如果接受互斥锁被禁用,则递减禁用计数器</span><br> <span class="hljs-keyword">if</span> (ngx_accept_disabled > <span class="hljs-number">0</span>) {<br> ngx_accept_disabled--;<br><br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-comment">// 尝试获取接受互斥锁</span><br> <span class="hljs-keyword">if</span> (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {<br> <span class="hljs-keyword">return</span>;<br> }<br><br> <span class="hljs-comment">// 根据互斥锁是否已被持有来决定事件的处理方式</span><br> <span class="hljs-keyword">if</span> (ngx_accept_mutex_held) {<br> flags |= NGX_POST_EVENTS;<br><br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-comment">// 调整定时器以适应接受互斥锁的延迟</span><br> <span class="hljs-keyword">if</span> (timer == NGX_TIMER_INFINITE<br> || timer > ngx_accept_mutex_delay)<br> {<br> timer = ngx_accept_mutex_delay;<br> }<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><h3 id="5-3、实际操作"><a href="#5-3、实际操作" class="headerlink" title="5.3、实际操作"></a>5.3、实际操作</h3><p>nginx开启了1个master,2个work,且开启了reuseport</p><p><img src="/img/%E5%BC%80%E5%90%AFreuseport.png" alt="开启reuseport"></p><p>来看下没有启用reuseport的情况</p><p><img src="/img/%E5%A4%9A%E8%BF%9B%E7%A8%8B%E7%9B%91%E5%90%AC%E4%B8%80%E4%B8%AAsocket.png" alt="多进程监听一个socket"></p><p>可以明显看到开启reuseport后,不同的work会监听自己的socket。而根据实际的性能测试来看,开启reuseport可以明显提升性能。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>参考自:《深入理解linux网络》第三章<br>linux版本:6.12<br>nginx代码:1.27</p>]]></content>
<tags>
<tag>nginx,socket</tag>
<tag>epoll</tag>
</tags>
</entry>
<entry>
<title>ingress-nginx使用limit_except的问题</title>
<link href="/2024/08/25/Problems%20with%20ingress-nginx%20using%20limit_except/"/>
<url>/2024/08/25/Problems%20with%20ingress-nginx%20using%20limit_except/</url>
<content type="html"><![CDATA[<h2 id="1、错误的返回-503"><a href="#1、错误的返回-503" class="headerlink" title="1、错误的返回-503"></a>1、错误的返回-503</h2><p>起因是ingress-nginx有人提了一个issue:<a href="https://github.com/kubernetes/ingress-nginx/issues/11742">https://github.com/kubernetes/ingress-nginx/issues/11742</a> ,使用limit_except时没有得到预期的403,而是得到了503,首先看下怎么复现</p><p>首先得有一个kubernetes环境,启动一个服务节点foo,然后安装ingress-nginx,ingress的配置如下:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span><br><span class="hljs-attr">kind:</span> <span class="hljs-string">Ingress</span><br><span class="hljs-attr">metadata:</span><br> <span class="hljs-attr">name:</span> <span class="hljs-string">foo-ingress</span><br> <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span><br> <span class="hljs-attr">annotations:</span><br> <span class="hljs-attr">nginx.ingress.kubernetes.io/configuration-snippet:</span> <span class="hljs-string">limit_except</span> <span class="hljs-string">GET</span> { <span class="hljs-string">deny</span> <span class="hljs-string">all;</span> }<br> <span class="hljs-attr">nginx.ingress.kubernetes.io/server-snippet:</span> <span class="hljs-string">|</span><br><span class="hljs-string"> location =/ {</span><br><span class="hljs-string"> return 403;</span><br><span class="hljs-string"> }</span><br><span class="hljs-string"></span><span class="hljs-attr">spec:</span><br> <span class="hljs-attr">ingressClassName:</span> <span class="hljs-string">nginx</span><br> <span class="hljs-attr">rules:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span><br> <span class="hljs-attr">http:</span><br> <span class="hljs-attr">paths:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">/foo</span><br> <span class="hljs-attr">pathType:</span> <span class="hljs-string">Prefix</span><br> <span class="hljs-attr">backend:</span><br> <span class="hljs-attr">service:</span><br> <span class="hljs-attr">name:</span> <span class="hljs-string">foo-service</span><br> <span class="hljs-attr">port:</span><br> <span class="hljs-attr">number:</span> <span class="hljs-number">8080</span><br></code></pre></td></tr></table></figure><p>这个配置的含义是,只允许get请求请求foo,其余请求一律为403,比如一个POST请求</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">curl -X POST http://172.xx.xx.xx:32080/foo<br></code></pre></td></tr></table></figure><p>按照正常思维来说应该返回403,但是返回了503?</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs shell"><html><br><head><title>503 Service Temporarily Unavailable</title></head><br><body><br><center><h1>503 Service Temporarily Unavailable</h1></center><br><hr><center>nginx</center><br></body><br></html><br></code></pre></td></tr></table></figure><p>难道是nginx/openresty的问题吗?于是在openresty快速测试一下,配置如下</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-section">server</span> {<br> <span class="hljs-attribute">listen</span> <span class="hljs-number">5678</span>;<br> <span class="hljs-attribute">server_name</span> ZJfans.com;<br><br> <span class="hljs-section">location</span> / {<br> <span class="hljs-attribute">limit_except</span> GET {<br> <span class="hljs-attribute">deny</span> all;<br> }<br> <span class="hljs-attribute">return</span> <span class="hljs-number">200</span>;<br> }<br> <br> <span class="hljs-section">location</span> = / {<br> <span class="hljs-attribute">return</span> <span class="hljs-number">403</span>;<br> }<br> <br> <span class="hljs-section">location</span> = /foo {<br> <span class="hljs-attribute">limit_except</span> GET {<br> <span class="hljs-attribute">deny</span> all;<br> }<br> <span class="hljs-attribute">return</span> <span class="hljs-number">200</span>;<br> }<br> }<br></code></pre></td></tr></table></figure><p>测试的结果,post确实返回了403,所以openresty本身没有问题,那么问题就出在ingress-nginx</p><p><img src="/img/limit_except-%E5%9B%BE1.png" alt="openresty测试limit_except"></p><h2 id="2、ingress的nginx-conf配置"><a href="#2、ingress的nginx-conf配置" class="headerlink" title="2、ingress的nginx.conf配置"></a>2、ingress的nginx.conf配置</h2><p>现在来看下ingress生成的nginx.conf有什么特殊的,只看重点</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-section">location</span> = /foo { <br> <br> <span class="hljs-attribute">set</span> <span class="hljs-variable">$namespace</span> <span class="hljs-string">"default"</span>; <br> <span class="hljs-attribute">set</span> <span class="hljs-variable">$ingress_name</span> <span class="hljs-string">"foo-ingress"</span>;<br> <span class="hljs-attribute">set</span> <span class="hljs-variable">$service_name</span> <span class="hljs-string">"foo-service"</span>; <br> <span class="hljs-attribute">set</span> <span class="hljs-variable">$service_port</span> <span class="hljs-string">"8080"</span>; <br> <span class="hljs-attribute">set</span> <span class="hljs-variable">$location_path</span> <span class="hljs-string">"/foo"</span>;<br> <span class="hljs-attribute">set</span> <span class="hljs-variable">$global_rate_limit_exceeding</span> n; <br> <br> <span class="hljs-section">rewrite_by_lua_block</span> { <br> lua_ingress.rewrite({ <br> <span class="hljs-attribute">force_ssl_redirect</span> = <span class="hljs-literal">false</span>, <br> ssl_redirect = <span class="hljs-literal">true</span>, <br> force_no_ssl_redirect = <span class="hljs-literal">false</span>,<br> preserve_trailing_slash = <span class="hljs-literal">false</span>,<br> use_port_in_redirects = <span class="hljs-literal">false</span>,<br> global_throttle = { <span class="hljs-attribute">namespace</span> = <span class="hljs-string">""</span>, limit = <span class="hljs-number">0</span>, window_size = <span class="hljs-number">0</span>, key = { }, <span class="hljs-attribute">ignored_cidrs</span> = { } },<br> }) <br> balancer.rewrite() <br> plugins.run() <br> }<br> <br> <span class="hljs-attribute">set</span> <span class="hljs-variable">$proxy_upstream_name</span> <span class="hljs-string">"default-foo-service-8080"</span>;<br> <br> <span class="hljs-comment"># ............................................... </span><br> <span class="hljs-comment"># ..................忽略海量配置............................. </span><br> <span class="hljs-comment"># ............................................... </span><br> <br> <span class="hljs-attribute">limit_except</span> GET { <span class="hljs-attribute">deny</span> all; } <br> <br> <br> <span class="hljs-attribute">proxy_pass</span> http://upstream_balancer; <br> <br> <span class="hljs-attribute">proxy_redirect</span> <span class="hljs-literal">off</span>; <br> <br> }<br></code></pre></td></tr></table></figure><p>测试了一下,我发现balancer.rewrite()是问题的关键,这其实是balancer初始化的一个实例,那么我们来看看这块代码</p><h2 id="3、balancer-lua"><a href="#3、balancer-lua" class="headerlink" title="3、balancer.lua"></a>3、balancer.lua</h2><p>可以看到这里确实返回了503,那么为什么get_balancer()会失败呢?</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_M.rewrite</span><span class="hljs-params">()</span></span><br> <span class="hljs-keyword">local</span> balancer = get_balancer()<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> balancer <span class="hljs-keyword">then</span><br> ngx.<span class="hljs-built_in">status</span> = ngx.HTTP_SERVICE_UNAVAILABLE<br> <span class="hljs-keyword">return</span> ngx.<span class="hljs-built_in">exit</span>(ngx.<span class="hljs-built_in">status</span>)<br> <span class="hljs-keyword">end</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><p>现在来看看get_balancer的实现,其实就是,没有获取到balancer返回了nil</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-keyword">local</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get_balancer</span><span class="hljs-params">()</span></span><br> <span class="hljs-keyword">if</span> ngx.ctx.balancer <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> ngx.ctx.balancer<br> <span class="hljs-keyword">end</span><br><br> <span class="hljs-keyword">local</span> backend_name = ngx.var.proxy_upstream_name<br><br> <span class="hljs-keyword">local</span> balancer = balancers[backend_name]<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> balancer <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span><br> <span class="hljs-keyword">end</span><br><br> <span class="hljs-comment">-- ......................省略............................</span><br><br> <span class="hljs-keyword">return</span> balancer<br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><p>现在来看为什么没有获取到balancer,首先使用ngx.say输出一下backend_name,我发现是一个 ”-“,这其实很奇怪,因为nginx.conf是这么设置的</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-attribute">set</span> <span class="hljs-variable">$proxy_upstream_name</span> <span class="hljs-string">"default-foo-service-8080"</span>;<br></code></pre></td></tr></table></figure><p>照理ngx.var.proxy_upstream_name可以取到的值是default-foo-service-8080,那为什么会变成 - ?这又到了openresty的问题了,这时我们需要研究一下limit_except的源码</p><h2 id="4、limit-except代码"><a href="#4、limit-except代码" class="headerlink" title="4、limit_except代码"></a>4、limit_except代码</h2><p>简单研究一下,直接来看关键的代码</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_http_update_location_config</span><span class="hljs-params">(<span class="hljs-type">ngx_http_request_t</span> *r)</span><br>{<br> <span class="hljs-type">ngx_http_core_loc_conf_t</span> *clcf;<br><br> clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);<br><br> <span class="hljs-keyword">if</span> (r->method & clcf->limit_except) {<br> r->loc_conf = clcf->limit_except_loc_conf;<br> clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);<br> }<br><span class="hljs-comment">// ..................................省略....................................</span><br><br>}<br></code></pre></td></tr></table></figure><p>关键在于r->loc_conf = clcf->limit_except_loc_conf;这里切换了r的loc,配置进行了更新,在 Nginx 中,<code>r->loc_conf = clcf->limit_except_loc_conf;</code> 这行代码用于处理特定请求方法时的配置切换。具体来说,当客户端请求的 HTTP 方法不在 <code>limit_except</code> 指定的允许范围内时,Nginx 会将该请求的 <code>loc_conf</code>(location 配置)切换到为该方法配置的 <code>limit_except_loc_conf</code>。</p><p><strong>举个具体的例子:</strong></p><p>假设有如下配置:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-section">server</span> {<br> <span class="hljs-attribute">listen</span> <span class="hljs-number">5678</span>;<br> <span class="hljs-attribute">server_name</span> ZJfans.com;<br><br> <span class="hljs-section">location</span> / {<br> <span class="hljs-attribute">limit_except</span> GET {<br> <span class="hljs-attribute">deny</span> all;<br> }<br> <span class="hljs-attribute">return</span> <span class="hljs-number">200</span>;<br> }<br> <br> <span class="hljs-section">location</span> = / {<br> <span class="hljs-attribute">return</span> <span class="hljs-number">403</span>;<br> }<br> <br> <span class="hljs-section">location</span> /foo {<br> <span class="hljs-attribute">set</span> <span class="hljs-variable">$proxy_upstream_name</span> <span class="hljs-string">"default-foo-service-8080"</span>;<br><br> <span class="hljs-section">rewrite_by_lua_block</span> {<br> <span class="hljs-attribute">local</span> upstream_name = ngx.var.proxy_upstream_name<br> ngx.log(ngx.INFO, <span class="hljs-string">"Proxy upstream name: "</span>, upstream_name)<br> }<br><br> limit_except GET {<br> <span class="hljs-attribute">deny</span> all;<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><p>在这个配置中:</p><ol><li><p><strong>配置结构</strong>:</p><ul><li><code>clcf->limit_except</code> 保存了限制条件,即允许的 HTTP 方法,这里是 <code>GET</code> 和 <code>POST</code>。</li><li><code>clcf->limit_except_loc_conf</code> 是一个指向 <code>limit_except</code> 条件下的特定 location 配置结构的指针。</li></ul></li><li><p><strong>请求处理</strong>:</p><ul><li>当一个请求到达 <code>/foo</code> 路径时,Nginx 首先会根据请求的方法来检查 <code>clcf->limit_except</code> 中的配置。</li><li>如果请求方法是 <code>GET</code> ,则继续使用原始的 <code>loc_conf</code> 处理请求,执行 <code>rewrite_by_lua_block</code> 等其他配置。</li><li>如果请求方法是其他方法(如 <code>DELETE</code> 或 <code>POST</code>),代码中的 <code>if (r->method & clcf->limit_except)</code> 条件会成立,Nginx 会将 <code>r->loc_conf</code> 切换到 <code>clcf->limit_except_loc_conf</code>,即对应限制方法的特定配置。这时,Nginx 可能会直接拒绝请求或应用不同的配置。</li></ul></li><li><p><strong>具体行为</strong>:</p><ul><li>当请求方法是 <code>GET</code> 或时,<code>r->loc_conf</code> 保持为原始的配置,执行 <code>rewrite_by_lua_block</code>,可以打印 <code>proxy_upstream_name</code>。</li><li>当请求方法是 <code>POST</code>,<code>r->loc_conf</code> 切换为 <code>limit_except_loc_conf</code>,此时不再执行 <code>rewrite_by_lua_block</code>,直接应用 <code>deny all</code>,返回 403 错误。</li></ul></li></ol><p><strong>所以如果请求方法是 <code>POST</code>,ngx.var.proxy_upstream_name根本就获取不到值,因为此时配置更新为limit_except的loc,在locatioon设置的变量都无法访问到,nginx会报错<code>using uninitialized</code>,此时问题已经定位到了,这是nginx的限制</strong></p><p><img src="/img/limit_except-%E5%9B%BE2.png" alt="获取location设置的变量失败"></p><p>再深入一下,如果把set $proxy_upstream_name “default-foo-service-8080”;设置为server级别呢,是否可以获取到这个变量?</p><p>答案是可以的,因为现在只是替换了location级别的配置,server级别的配置并没有更新,可以正常获取</p><h2 id="5、验证–调试balancer-lua"><a href="#5、验证–调试balancer-lua" class="headerlink" title="5、验证–调试balancer.lua"></a>5、验证–调试balancer.lua</h2><h3 id="5-1、”-“-从哪里来的"><a href="#5-1、”-“-从哪里来的" class="headerlink" title="5.1、”-“ 从哪里来的"></a>5.1、”-“ 从哪里来的</h3><p>来看nginx.conf的配置,server里面设置了默认值,所以 - 就是从这里来的</p><p><img src="/img/limit_except-%E5%9B%BE3.png" alt="proxy_upstream_name设置初始值为"-""></p><h3 id="5-2、balancers结构"><a href="#5-2、balancers结构" class="headerlink" title="5.2、balancers结构"></a>5.2、balancers结构</h3><p>回到balancer.lua,我对balancers的结构体挺好奇的,于是打印一下,看下具体的配置</p><p>1、查看</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs lua">ocal <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get_balancer</span><span class="hljs-params">()</span></span> <br> <span class="hljs-keyword">if</span> ngx.ctx.balancer <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> ngx.ctx.balancer <br> <span class="hljs-keyword">end</span> <br> <br> <span class="hljs-keyword">local</span> backend_name = ngx.var.proxy_upstream_name <br> <br><span class="hljs-comment">---------------------------------------begin---------------------------------</span><br> backend_name = <span class="hljs-string">"default-foo-service-8080"</span> <span class="hljs-comment">--设置正确的值</span><br> <br> <span class="hljs-keyword">local</span> balancer = balancers[backend_name] <br> <br> <span class="hljs-keyword">if</span> balancer <span class="hljs-keyword">then</span> <span class="hljs-comment">--返回数据结构 </span><br> <span class="hljs-keyword">local</span> balancer_json = cjson.encode(balancer) <br> ngx.header[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span><br> ngx.<span class="hljs-built_in">status</span> = ngx.HTTP_OK <br> ngx.say(balancer_json) <br> <span class="hljs-keyword">end</span><br><span class="hljs-comment">---------------------------------------end-----------------------------------</span><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> balancer <span class="hljs-keyword">then</span> <br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span> <br> <span class="hljs-keyword">end</span> <br></code></pre></td></tr></table></figure><p>请求/响应</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">curl -X POST http://172.xx.xx.xx:32080/foo<br><br>{"traffic_shaping_policy":{"weight":0,"cookie":"","headerPattern":"","headerValue":"","header":"","weightTotal":0},"instance":{"nodes":{"10.233.xx.xx:5678":1},"gcd":1,"only_key":"10.233.xx.xx:5678","cw":1,"last_id":"10.233.xx.xx:5678","max_weight":1}}<br></code></pre></td></tr></table></figure><p>所以如果正常能取到proxy_upstream_name的值,也是能取到balancer的</p><h2 id="6、如何修复"><a href="#6、如何修复" class="headerlink" title="6、如何修复"></a>6、如何修复</h2><p>按照我的初步想法,简单粗暴,在proxy_upstream_name没有被重新赋值时,直接返回403。因为使用了limit_except 后,location设置的proxy_upstream_name将无法被获取,肯定为 - ,所以可以认为只要是 -,就是使用了limit_except?返回403也比较合理。</p><p>唯一的问题是,会不会有其他指令,也会导致这个情况,但是他需要返回其他的状态码,这个暂时还没想到有,nginx的指令很多,需要慢慢确认</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-keyword">local</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get_balancer</span><span class="hljs-params">()</span></span><br> <span class="hljs-keyword">if</span> ngx.ctx.balancer <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> ngx.ctx.balancer<br> <span class="hljs-keyword">end</span><br><br> <span class="hljs-keyword">local</span> backend_name = ngx.var.proxy_upstream_name<br><span class="hljs-comment">---------------------------------------begin---------------------------------</span><br> <span class="hljs-keyword">if</span> backend_name == <span class="hljs-string">'-'</span> <span class="hljs-keyword">then</span><br> ngx.<span class="hljs-built_in">status</span> = ngx.HTTP_FORBIDDEN<br> <span class="hljs-keyword">return</span> ngx.<span class="hljs-built_in">exit</span>(ngx.<span class="hljs-built_in">status</span>) <br> <span class="hljs-keyword">end</span><br><span class="hljs-comment">---------------------------------------end-----------------------------------</span><br> <span class="hljs-keyword">local</span> balancer = balancers[backend_name]<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> balancer <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span><br> <span class="hljs-keyword">end</span><br><span class="hljs-comment">-- ......................省略............................</span><br> <span class="hljs-keyword">return</span> balancer<br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><p>–</p><p>已经向社区反馈该问题的进展,结果后续会更新</p>]]></content>
<tags>
<tag>ingress-nginx</tag>
<tag>limit_except</tag>
</tags>
</entry>
<entry>
<title>gcc编译选项--fcommon</title>
<link href="/2024/08/17/gcc%E7%BC%96%E8%AF%91%E9%80%89%E9%A1%B9--fcommon/"/>
<url>/2024/08/17/gcc%E7%BC%96%E8%AF%91%E9%80%89%E9%A1%B9--fcommon/</url>
<content type="html"><![CDATA[<h2 id="multiple-definition"><a href="#multiple-definition" class="headerlink" title="multiple definition"></a>multiple definition</h2><p>最近在龙蜥操作系统编译时,出现了multiple definition的错误,重复定义?查看了代码,有几个变量确实是重复定义了,简单来讲就是在一个.h文件定义了一个变量,多个.c文件又包含了这个.h,导致这个变量在多个.c文件重复定义,在编译时报错。</p><p>修复也很简单,.h文件的中的变量统一加extern,然后只有一个.c文件才能定义初始化这个变量,这样就可以避免重复定义,也顺利编译成功。</p><h2 id="gcc的版本"><a href="#gcc的版本" class="headerlink" title="gcc的版本"></a>gcc的版本</h2><p>代码确实存在问题,不过其他操作系统并不会报错,只是一个警告,因此我很好奇这是为什么。网上搜索后,发现了一篇很好的文章:<a href="https://club.rt-thread.org/ask/article/5fb1ecf297a83492.html">https://club.rt-thread.org/ask/article/5fb1ecf297a83492.html</a> ,gcc版本在10版本后,关闭了 fcommon选项,使用了fno-common,来看看这个选项的描述:</p><figure class="highlight livecodeserver"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs livecodeserver">-fcommon<br>In C code, this option controls <span class="hljs-keyword">the</span> placement <span class="hljs-keyword">of</span> <span class="hljs-built_in">global</span> variables defined <span class="hljs-keyword">without</span> <span class="hljs-keyword">an</span> initializer, known <span class="hljs-keyword">as</span> tentative definitions <span class="hljs-keyword">in</span> <span class="hljs-keyword">the</span> C standard. Tentative definitions are distinct <span class="hljs-built_in">from</span> declarations <span class="hljs-keyword">of</span> <span class="hljs-keyword">a</span> <span class="hljs-built_in">variable</span> <span class="hljs-keyword">with</span> <span class="hljs-keyword">the</span> extern keyword, which <span class="hljs-built_in">do</span> <span class="hljs-keyword">not</span> allocate storage.<br><br>The default is -fno-common, which specifies that <span class="hljs-keyword">the</span> compiler places uninitialized <span class="hljs-built_in">global</span> variables <span class="hljs-keyword">in</span> <span class="hljs-keyword">the</span> BSS section <span class="hljs-keyword">of</span> <span class="hljs-keyword">the</span> object <span class="hljs-built_in">file</span>. This inhibits <span class="hljs-keyword">the</span> merging <span class="hljs-keyword">of</span> tentative definitions <span class="hljs-keyword">by</span> <span class="hljs-keyword">the</span> linker so you <span class="hljs-built_in">get</span> <span class="hljs-keyword">a</span> multiple-definition error <span class="hljs-keyword">if</span> <span class="hljs-keyword">the</span> same <span class="hljs-built_in">variable</span> is accidentally defined <span class="hljs-keyword">in</span> more than <span class="hljs-literal">one</span> compilation unit.<br><br>The -fcommon places uninitialized <span class="hljs-built_in">global</span> variables <span class="hljs-keyword">in</span> <span class="hljs-keyword">a</span> common block. This allows <span class="hljs-keyword">the</span> linker <span class="hljs-built_in">to</span> <span class="hljs-built_in">resolve</span> all tentative definitions <span class="hljs-keyword">of</span> <span class="hljs-keyword">the</span> same <span class="hljs-built_in">variable</span> <span class="hljs-keyword">in</span> different compilation units <span class="hljs-built_in">to</span> <span class="hljs-keyword">the</span> same object, <span class="hljs-keyword">or</span> <span class="hljs-built_in">to</span> <span class="hljs-keyword">a</span> non-tentative definition. This behavior is inconsistent <span class="hljs-keyword">with</span> C++, <span class="hljs-keyword">and</span> <span class="hljs-keyword">on</span> <span class="hljs-title">many</span> <span class="hljs-title">targets</span> <span class="hljs-title">implies</span> <span class="hljs-title">a</span> <span class="hljs-title">speed</span> <span class="hljs-title">and</span> <span class="hljs-title">code</span> <span class="hljs-title">size</span> <span class="hljs-title">penalty</span> <span class="hljs-title">on</span> <span class="hljs-title">global</span> <span class="hljs-title">variable</span> <span class="hljs-title">references</span>. <span class="hljs-title">It</span> <span class="hljs-title">is</span> <span class="hljs-title">mainly</span> <span class="hljs-title">useful</span> <span class="hljs-title">to</span> <span class="hljs-title">enable</span> <span class="hljs-title">legacy</span> <span class="hljs-title">code</span> <span class="hljs-title">to</span> <span class="hljs-title">link</span> <span class="hljs-title">without</span> <span class="hljs-title">errors</span>.<br></code></pre></td></tr></table></figure><p>文章已经讲解很清楚,这里作为我的理解</p><p><code>-fno-common</code></p><ul><li><strong>作用</strong>:当使用<code>-fno-common</code>选项时,编译器将未初始化的全局变量放置在BSS段中。</li><li><strong>链接器行为</strong>:未初始化的全局变量会被视为强符号。在链接阶段,如果相同的未初始化全局变量在多个编译单元中被定义,链接器会将其视为重复定义,并报告错误。也就是说,链接器不会合并这些变量的定义,任何多个定义的冲突都会导致链接错误。</li></ul><p><code>-fcommon</code></p><ul><li><strong>作用</strong>:当使用<code>-fcommon </code>选项时,编译器会将未初始化的全局变量放入一个<code>COMMON</code>块中。</li><li><strong>链接器行为</strong>:<code>COMMON</code>块是一种特殊的内存区域,允许链接器在链接阶段合并同名的未初始化全局变量的定义。即使这些变量在多个编译单元中被定义,链接器会将它们合并为一个单一的变量对象。</li></ul><p>也就是使用了fcommon,未初始化的全局变量放在<code>COMMON</code>块,允许重复定义,链接期会合并为1个。</p><p>GCC 默认的链接器行为是,如果在链接过程中发现重复的符号,它会选择第一个找到的符号,并忽略后续的符号。这意味着,链接器会使用第一个定义的符号(包括它的数据类型),而忽略后续定义的符号。但是2个变量使用的内存初始地址是一样的,也就是一个int a和double a其实共用一块内存,int占4个字节,double占8个字节,这样其实可能导致问题。int a中可能还有一个int b,这个b占用了a后面的4个字节,2个变量(b 和 double a)使用了一块内存,肯定会有问题。</p><p>而使用了fno-common属性后,未初始化的全局变量现在会放到BSS段,属于强符号,不允许重复定义</p><h2 id="内存模型"><a href="#内存模型" class="headerlink" title="内存模型"></a>内存模型</h2><p>C语言程序的内存通常被分为几个主要区域:</p><ul><li><strong>Text段</strong>(代码段):<ul><li><strong>作用</strong>:存放程序的可执行代码,包括所有函数和指令。</li><li><strong>属性</strong>:通常是只读的,以防止程序代码在运行时被修改,并且可以在多个进程间共享。</li></ul></li><li><strong>Data段</strong>(数据段):<ul><li><strong>作用</strong>:存放已初始化的全局变量和静态变量。<ul><li><strong><code>.data</code>段</strong>:包含初始化的全局和静态变量。</li><li><strong><code>.rodata</code>段</strong>:包含只读的初始化数据(如字符串常量)。</li></ul></li></ul></li><li><strong>BSS段</strong>:<ul><li><strong>作用</strong>:存放未初始化的全局变量和静态变量。程序加载时,这部分内存会被自动初始化为0。</li><li><strong>特点</strong>:在可执行文件中不占用实际空间,只有在程序运行时才分配内存。</li></ul></li><li><strong>堆</strong>(Heap):<ul><li><strong>作用</strong>:动态分配内存区域,通过函数如<code>malloc()</code>和<code>free()</code>进行管理。用于存放动态分配的数据结构。</li><li><strong>管理</strong>:需要程序员手动管理内存分配和释放,避免内存泄漏和悬挂指针。</li></ul></li><li><strong>栈</strong>(Stack):<ul><li><strong>作用</strong>:存放局部变量、函数参数和返回地址。每个函数调用时会在栈上分配一个栈帧。</li><li><strong>特点</strong>:栈内存的分配和释放由系统自动管理。栈大小通常有限,如果超出栈空间会导致栈溢出。</li></ul></li></ul><h2 id="强符号-弱符号"><a href="#强符号-弱符号" class="headerlink" title="强符号/弱符号"></a>强符号/弱符号</h2><p>对于全局变量来说,如果初始化了不为0的值,那么该全局变量则被保存在data段,如果初始化的值为0,那么将其保存在bss段,如果没有初始化,则将其保存在common段,等到链接时再将其放入到bss段。关于第三点不同编译器行为会不同,有的编译器会把没有初始化的全局变量直接放到bss段,也就是gcc的-fcommon与-fno-common属性的差异。</p><p>绝大多数情况下,函数和已初始化的变量是强符号,而未初始化的变量是弱符号。对于它们,下列三条规则适用:</p><ol><li>同名的强符号只能存在一个。</li><li>一个强符号可以和多个同名的弱符号共存,但调用时会选择强符号的值。</li><li>有多个弱符号时,链接器可以选择其中任意一个。</li></ol><h2 id="librdkafka的编译问题?"><a href="#librdkafka的编译问题?" class="headerlink" title="librdkafka的编译问题?"></a>librdkafka的编译问题?</h2><p>目前librdkafka的最新版本是2.5,最近需要升级这个库,于是我在十几个操作系统编译了一下这个库,有3个编译报错,其中一个是因为依赖的libssl没找到,于是我在lib64指定了一下libssl.so也就编译通过了,而官方最近刚修复了这个问题,也就是没找ssl依赖的情况下,也能编译成功,有点巧。</p><p>还有2个报了<strong>redefinition of typedef</strong>的错误,一个是suse sp4,一个是centos 6,github搜了一下,发现这是老问题,这个项目以前经常会遇到这个错误,看着是修复过了,现在还会有错误?这2个操作系统的gcc都是4.4,版本比较低,而其他编译过的操作系统,gcc有4.8、7.3、8.3、10.3、12.3,也就是说旧的gcc反而会报错。找了一个变量看下代码</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">rd_kafka_toppar_s</span> <span class="hljs-title">rd_kafka_toppar_t</span>;</span><br></code></pre></td></tr></table></figure><p>在<strong>rdkafka_int.h</strong>和<strong>rdkafka_op.h</strong>都定义了这个变量,然后 <strong>rdkafka_op.c</strong>都包含了这个头文件,很明显是重复定义了。但是这个符合c语言规则的</p><p>ISO/IEC 9899:1999 6.7.3中是这么描述的</p><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scala"><span class="hljs-type">If</span> an identifier has no linkage, there shall be no more than one declaration of the identifier<br>(in a declarator or <span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-title">specifier</span>) <span class="hljs-keyword">with</span> <span class="hljs-title">the</span> <span class="hljs-title">same</span> <span class="hljs-title">scope</span> <span class="hljs-title">and</span> <span class="hljs-title">in</span> <span class="hljs-title">the</span> <span class="hljs-title">same</span> <span class="hljs-title">name</span> <span class="hljs-title">space</span>, <span class="hljs-title">except</span></span><br><span class="hljs-keyword">for</span> tags as specified in <span class="hljs-number">6.7</span><span class="hljs-number">.2</span><span class="hljs-number">.3</span>.<br></code></pre></td></tr></table></figure><p>而c99中</p><figure class="highlight livecodeserver"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs livecodeserver">If <span class="hljs-keyword">the</span> same qualifier appears more than once <span class="hljs-keyword">in</span> <span class="hljs-keyword">the</span> same specifier-qualifier-list, either<br>directly <span class="hljs-keyword">or</span> via <span class="hljs-literal">one</span> <span class="hljs-keyword">or</span> more typedefs, <span class="hljs-keyword">the</span> behavior is <span class="hljs-keyword">the</span> same <span class="hljs-keyword">as</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">it</span> appeared only<br>once.<br></code></pre></td></tr></table></figure><p>其实就是以前的c标准不允许重复重复声明,c99开始允许了,因此代码这么写没有问题,只是老版本的gcc遵循老的c标准,编译会报错。但是librdkafka明确表示以后不支持centos 6和7,所以也没什么好说的。8的gcc版本一般为8.3,可以正常编译,后续其实要推动客户升级操作系统,毕竟后续会出现越来越多的兼容性问题。</p>]]></content>
<tags>
<tag>gcc</tag>
<tag>fcommon</tag>
<tag>fno-common</tag>
</tags>
</entry>
<entry>
<title>chunked编码格式引发的问题</title>
<link href="/2024/08/13/chunked%E7%BC%96%E7%A0%81%E6%A0%BC%E5%BC%8F%E5%BC%95%E5%8F%91%E7%9A%84%E9%97%AE%E9%A2%98/"/>
<url>/2024/08/13/chunked%E7%BC%96%E7%A0%81%E6%A0%BC%E5%BC%8F%E5%BC%95%E5%8F%91%E7%9A%84%E9%97%AE%E9%A2%98/</url>
<content type="html"><![CDATA[<h2 id="1、问题"><a href="#1、问题" class="headerlink" title="1、问题"></a>1、问题</h2><p>在适配某家的cas时,票据校验时一直失败,通常是以下几个问题引发的问题<br>1、网关与cas服务器的网络不通,导致票校验的请求发送不出去<br>2、cas服务器的票据校验接口,配置了域名,但是在nginx没有配置dns,导致域名无法解析,请求无法发送</p><p>3、配置了ip,但是cas服务端限制了host,禁用了ip访问</p><p>4、票据校验的请求有问题,service参数组装不正确</p><p>经过排查后,发现都不是上面的原因,因此抓了一个包,发现cas服务端是正常把响应返回回来的</p><h2 id="2、代码分析"><a href="#2、代码分析" class="headerlink" title="2、代码分析"></a>2、代码分析</h2><h3 id="2-1、定位"><a href="#2-1、定位" class="headerlink" title="2.1、定位"></a>2.1、定位</h3><p>既然cas服务端正常返回了响应,那就是网关侧有异常,代码打印状态码和body,发现状态码确实200,但是body是nil,也就是没有获取到body。我们采用了agentzh” Yichun“ 写的lua-resty-http模块(<a href="https://github.com/liseen/lua-resty-http">https://github.com/liseen/lua-resty-http</a>)</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-keyword">local</span> xxx = hc:request{<br> url = cas_validate_uri,<br> method = <span class="hljs-string">"GET"</span>,<br>}<br><br><span class="hljs-comment">--结果调用的是</span><br><br><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">request</span><span class="hljs-params">(self, reqt)</span></span><br></code></pre></td></tr></table></figure><p>其中接收响应body的代码是</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- receive body</span><br> <span class="hljs-keyword">if</span> shouldreceivebody(nreqt, code) <span class="hljs-keyword">then</span><br> body, err = receivebody(sock, headers, nreqt) <span class="hljs-comment">--接收body</span><br> <span class="hljs-keyword">if</span> err <span class="hljs-keyword">then</span><br> sock:<span class="hljs-built_in">close</span>()<br> <span class="hljs-keyword">if</span> code == <span class="hljs-number">200</span> <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>, code, headers, <span class="hljs-built_in">status</span>, <span class="hljs-literal">nil</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-string">"read body failed "</span> .. err<br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><p>所以我们重点分析receivebody</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-keyword">local</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">receivebody</span><span class="hljs-params">(sock, headers, nreqt)</span></span><br> <span class="hljs-comment">-- 定义一个名为 receivebody 的本地函数,接收三个参数:</span><br> <span class="hljs-comment">-- sock:表示连接的套接字对象,用于读取数据。</span><br> <span class="hljs-comment">-- headers:表示请求的头部信息。</span><br> <span class="hljs-comment">-- nreqt:包含配置参数的表格,例如最大允许的主体大小和回调函数。</span><br><br> <span class="hljs-keyword">local</span> t = headers[<span class="hljs-string">"transfer-encoding"</span>] <span class="hljs-comment">-- 获取 "transfer-encoding" 头部的值并存储在变量 t 中</span><br> <span class="hljs-keyword">local</span> body = {} <span class="hljs-comment">-- 用于存储响应体的数据块的表格</span><br> <span class="hljs-keyword">local</span> callback = nreqt.body_callback <span class="hljs-comment">-- 获取 nreqt 中的 body_callback 函数</span><br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> callback <span class="hljs-keyword">then</span><br> <span class="hljs-comment">-- 如果没有提供回调函数</span><br><br> <span class="hljs-keyword">local</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bc</span><span class="hljs-params">(data, chunked_header, ...)</span></span><br> <span class="hljs-comment">-- 定义一个本地回调函数 bc,用于处理接收到的数据块</span><br> <span class="hljs-keyword">if</span> chunked_header <span class="hljs-keyword">then</span> <span class="hljs-keyword">return</span> <span class="hljs-keyword">end</span><br> <span class="hljs-comment">-- 如果存在 chunked_header,则返回,不处理数据块</span><br> body[#body+<span class="hljs-number">1</span>] = data<br> <span class="hljs-comment">-- 将数据块添加到 body 表格中</span><br> <span class="hljs-keyword">end</span><br><br> callback = bc<br> <span class="hljs-comment">-- 将本地定义的回调函数赋值给 callback</span><br> <span class="hljs-keyword">end</span><br><br> <span class="hljs-keyword">if</span> t <span class="hljs-keyword">and</span> t ~= <span class="hljs-string">"identity"</span> <span class="hljs-keyword">then</span><br> <span class="hljs-comment">-- 如果 "transfer-encoding" 头部存在且其值不等于 "identity":</span><br> <span class="hljs-comment">-- 表示响应体是分块传输编码(chunked)</span><br><br> <span class="hljs-keyword">while</span> <span class="hljs-literal">true</span> <span class="hljs-keyword">do</span><br> <span class="hljs-comment">-- 开始一个无限循环,处理每个块</span><br><br> <span class="hljs-keyword">local</span> chunk_header = sock:receiveuntil(<span class="hljs-string">"\r\n"</span>)<br> <span class="hljs-comment">-- 调用 sock 对象的 receiveuntil 方法,读取直到 "\r\n" 为止的数据,表示读取块头部信息。</span><br> <br> <span class="hljs-keyword">local</span> data, err, partial = chunk_header()<br> <span class="hljs-comment">-- 调用 chunk_header 函数以获取数据块头部。如果成功,将数据块头部内容存储在 data 中。</span><br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> data <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err<br> <span class="hljs-comment">-- 如果 data 为 nil,表示读取失败,返回错误信息。</span><br> <span class="hljs-keyword">else</span><br> <span class="hljs-keyword">if</span> data == <span class="hljs-string">"0"</span> <span class="hljs-keyword">then</span><br> <span class="hljs-comment">-- 如果读取到的块头部为 "0",表示传输结束。</span><br><br> <span class="hljs-keyword">return</span> <span class="hljs-built_in">table</span>.<span class="hljs-built_in">concat</span>(body) <span class="hljs-comment">-- 将 body 中的数据块合并成一个字符串并返回</span><br> <span class="hljs-keyword">else</span><br> <span class="hljs-keyword">local</span> length = <span class="hljs-built_in">tonumber</span>(data, <span class="hljs-number">16</span>)<br> <span class="hljs-comment">-- 否则,将块头部内容转换为十六进制表示的长度。</span><br><br> <span class="hljs-comment">-- TODO check nreqt.max_body_size !!</span><br> <span class="hljs-comment">-- 注释:需要检查块的大小是否超过最大允许的主体大小。</span><br><br> <span class="hljs-keyword">local</span> ok, err = read_body_data(sock, length, nreqt.fetch_size, callback)<br> <span class="hljs-comment">-- 调用 read_body_data 函数从套接字读取指定长度的数据,并处理它。</span><br> <span class="hljs-comment">-- sock:表示连接的套接字对象。</span><br> <span class="hljs-comment">-- length:数据块的长度。</span><br> <span class="hljs-comment">-- nreqt.fetch_size:每次读取数据的大小。</span><br> <span class="hljs-comment">-- callback:读取后的回调函数。</span><br><br> <span class="hljs-keyword">if</span> err <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err<br> <span class="hljs-comment">-- 如果读取失败,返回错误信息。</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">elseif</span> headers[<span class="hljs-string">"content-length"</span>] ~= <span class="hljs-literal">nil</span> <span class="hljs-keyword">and</span> <span class="hljs-built_in">tonumber</span>(headers[<span class="hljs-string">"content-length"</span>]) >= <span class="hljs-number">0</span> <span class="hljs-keyword">then</span><br> <span class="hljs-comment">-- 如果 "transfer-encoding" 不存在或等于 "identity",且存在 "content-length" 头部且其值为非负数:</span><br> <span class="hljs-comment">-- 表示响应体的传输长度是固定的。</span><br><br> <span class="hljs-keyword">local</span> length = <span class="hljs-built_in">tonumber</span>(headers[<span class="hljs-string">"content-length"</span>])<br> <span class="hljs-comment">-- 将 "content-length" 的值转换为数值。</span><br><br> <span class="hljs-keyword">if</span> length > nreqt.max_body_size <span class="hljs-keyword">then</span><br> <span class="hljs-comment">-- 如果内容长度大于最大允许的主体大小:</span><br><br> ngx.<span class="hljs-built_in">log</span>(ngx.INFO, <span class="hljs-string">'content-length > nreqt.max_body_size !! Tail it !'</span>)<br> <span class="hljs-comment">-- 记录日志提示该情况。</span><br><br> length = nreqt.max_body_size<br> <span class="hljs-comment">-- 将长度截取为最大允许的大小。</span><br> <span class="hljs-keyword">end</span><br><br> <span class="hljs-keyword">local</span> ok, err = read_body_data(sock, length, nreqt.fetch_size, callback)<br> <span class="hljs-comment">-- 调用 read_body_data 函数读取指定长度的数据,并处理它。</span><br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ok <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err<br> <span class="hljs-comment">-- 如果读取失败,返回错误信息。</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">else</span><br> <span class="hljs-comment">-- 如果既没有 "transfer-encoding" 头部,也没有 "content-length" 头部:</span><br> <span class="hljs-comment">-- 假设响应体会在连接关闭时结束。</span><br><br> <span class="hljs-keyword">local</span> ok, err = read_body_data(sock, nreqt.max_body_size, nreqt.fetch_size, callback)<br> <span class="hljs-comment">-- 调用 read_body_data 函数读取直到最大允许大小的数据,并处理它。</span><br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ok <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err<br> <span class="hljs-comment">-- 如果读取失败,返回错误信息。</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br><br> <span class="hljs-keyword">return</span> <span class="hljs-built_in">table</span>.<span class="hljs-built_in">concat</span>(body)<br> <span class="hljs-comment">-- 将 body 中的数据块合并成一个字符串并返回</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><p>抓包查看,响应的编码格式是 <strong>Transfer-Encoding:chunked</strong>,因此确认是走到了上述代码解析chunked的逻辑,在关键代码打印日志后,发现这里返回了错误,读取body失败</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> data <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>,err<br></code></pre></td></tr></table></figure><p>那么为什么读取会失败?</p><h3 id="2-2、什么是chunked类型的数据?"><a href="#2-2、什么是chunked类型的数据?" class="headerlink" title="2.2、什么是chunked类型的数据?"></a>2.2、什么是chunked类型的数据?</h3><p>HTTP 中的 <code>Chunked</code> 传输编码是一种在不预先知道响应数据大小的情况下进行数据传输的方式。通常在 HTTP 响应中,服务器会在头部发送 <code>Content-Length</code> 字段,指定即将发送的数据的总长度。然而,有时候服务器在生成响应内容时并不知道最终的内容长度,比如当内容是动态生成的。这时候,服务器可以使用 <code>Chunked</code> 传输编码。</p><p>Chunked 传输编码的基本工作原理</p><p>在 <code>Chunked</code> 传输编码中,响应体被分割成一系列块(chunks),每个块可以有不同的大小。每个块由两部分组成:</p><ol><li>块头部(Chunk Header): 该部分指定了块的大小(以16进制表示),后面紧跟着一个回车换行符(\r\n)。</li><li>数据块(Chunk Data): 紧跟在块头部之后的实际数据,数据块的长度由块头部指定。块数据后面也跟着一个回车换行符(\r\n)。</li></ol><p>响应的最后一个块是一个特殊的块,它的大小为 0,表示数据传输的结束。</p><p><strong>客户端解析 Chunked 数据</strong></p><p>客户端接收到 <code>Chunked</code> 编码的响应时,会逐块解析数据,直到遇到大小为 <code>0</code> 的块。具体的解析步骤如下:</p><ol><li>读取块头部,确定当前块的大小。</li><li>读取指定大小的数据块。</li><li>继续读取下一个块,重复步骤 1 和 2。</li><li>当遇到大小为 <code>0</code> 的块时,停止读取。</li></ol><p><strong>常规的内容传输</strong></p><p>在通常情况下,当服务器要发送响应时,会先计算好整个响应体的大小,并在 HTTP 响应头的 <code>Content-Length</code> 字段中告知客户端。例如,服务器在发送一个 HTML 页面时,可能会先生成整个页面内容,并且计算出它的大小,然后在响应头中设置 <code>Content-Length</code>,再将整个内容一次性发送给客户端。</p><p>这种方式的缺点是:</p><ol><li>延迟高:服务器必须等待整个内容生成完毕,才能开始发送。这增加了初始延迟。</li><li>不适合动态生成的内容:如果内容是逐步生成的(例如,来自数据库的查询结果或通过流处理的内容),服务器需要等到所有内容都生成后才能计算总大小并发送。</li></ol><p><strong><code>Chunked</code> 传输编码的优势</strong></p><p>使用 <code>Chunked</code> 传输编码,服务器不需要预先知道响应内容的总大小。相反,它可以在生成内容的同时逐步将内容以一块块的形式发送给客户端。</p><h3 id="2-3、分析"><a href="#2-3、分析" class="headerlink" title="2.3、分析"></a>2.3、分析</h3><p>了解了原理,那我们来看下服务端返回的数据是否正常,使用tcp追踪流展示数据</p><p><img src="/img/chunked-01.png" alt="chunked-01"></p><p>十六进制:</p><p><img src="/img/chunked-02.png" alt="chunked-02"></p><p>数据很明显有问题,让我们回到代码,只有读到一个0,才会认为数据结束了,也就是已经接收到了完整的数据,但是现在不是一个0,是0000,从十六进制看也很明显,我们需要读取到 <strong>30 od oa</strong>才会认为数据结束,但是现在是<strong>30 30 30 30 od oa</strong>。因此客户端会一直读数据,而从请求的响应头connection:close 可以知道,这个连接在传输完数据后会关闭。因此当连接关闭时, local data, err, partial = chunk_header()最后会报错,err其实是一个close。</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-keyword">local</span> chunk_header = sock:receiveuntil(<span class="hljs-string">"\r\n"</span>)<br> <span class="hljs-keyword">local</span> data, err, partial = chunk_header()<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> data <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>,err<br> <span class="hljs-keyword">else</span><br> <span class="hljs-keyword">if</span> data == <span class="hljs-string">"0"</span> <span class="hljs-keyword">then</span> <span class="hljs-comment">--只有读到一个0才会认为数据结束了</span><br> <span class="hljs-keyword">return</span> <span class="hljs-built_in">table</span>.<span class="hljs-built_in">concat</span>(body) <span class="hljs-comment">-- end of chunk</span><br> <span class="hljs-keyword">else</span><br> <span class="hljs-keyword">local</span> length = <span class="hljs-built_in">tonumber</span>(data, <span class="hljs-number">16</span>)<br><br> <span class="hljs-comment">-- TODO check nreqt.max_body_size !!</span><br><br> <span class="hljs-keyword">local</span> ok, err = read_body_data(sock,length, nreqt.fetch_size, callback)<br> <span class="hljs-keyword">if</span> err <span class="hljs-keyword">then</span><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>,err<br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><p>所以到这里,问题其实可以确认是cas返回的数据有问题</p><h2 id="3、问题解决"><a href="#3、问题解决" class="headerlink" title="3、问题解决"></a>3、问题解决</h2><h3 id="3-1、本地验证"><a href="#3-1、本地验证" class="headerlink" title="3.1、本地验证"></a>3.1、本地验证</h3><p>联系了cas的服务商,被告知这个cas是拿开源来用的,掌握程度一般,沟通下来比较困难🙄,刚好我对这个问题比较感兴趣,因此拿对应开源版本做一个验证<br>cas地址:<a href="https://github.com/apereo/cas/tree/5.3.x?tab=readme-ov-file">https://github.com/apereo/cas/tree/5.3.x?tab=readme-ov-file</a><br>安装参考文档:<a href="https://www.cnblogs.com/hellxz/p/15740935.html">https://www.cnblogs.com/hellxz/p/15740935.html</a><br>tomact:<a href="https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.93/bin/">https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.93/bin/</a><br>cas的war包:<a href="https://repo1.maven.org/maven2/org/apereo/cas/cas-server-webapp-tomcat/">https://repo1.maven.org/maven2/org/apereo/cas/cas-server-webapp-tomcat/</a></p><p>简单运行后,成功对接我们的网关,那么抓包看一下返回的数据</p><p><img src="/img/chunked-03.png" alt="chunked-03"></p><p>数据格式非常正确!</p><h3 id="3-2、最终的结论"><a href="#3-2、最终的结论" class="headerlink" title="3.2、最终的结论"></a>3.2、最终的结论</h3><p>那么现在有问题的场景,还是cas做了一定的改动?cas是用java实现的,这块对我有一定的研究成本,研究了下源码,最后追踪到了视图,但是没找到具体哪里可以设置传输编码,后面有时间再研究一下,还是先把问题还给服务商。</p>]]></content>
<tags>
<tag>http</tag>
<tag>Transfer-Encoding</tag>
<tag>chunked</tag>
</tags>
</entry>
<entry>
<title>SSO实践</title>
<link href="/2024/08/04/SSO/"/>
<url>/2024/08/04/SSO/</url>
<content type="html"><![CDATA[<p>在早期的互联网发展时期,用户使用的系统很少,每个系统都有自己的登录系统,用户使用每个系统需要分别进行登录。随着互联网的发展,同一产品下耦合的系统越来越多,这时还需要用户在每个系统都进行登录,这对用户的体验感很不好,这时就需要一种方案可以解决多个相互信任系统需要多次登录的问题,此时单点登录应用而生。</p><p>单点登录全称Single Sign On,简称SSO。它的作用是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的系统。目前有很多种实现方式,本文将介绍3种常见的方式</p><h2 id="1、cas单点登录"><a href="#1、cas单点登录" class="headerlink" title="1、cas单点登录"></a>1、cas单点登录</h2><h3 id="1-1、什么是CAS"><a href="#1-1、什么是CAS" class="headerlink" title="1.1、什么是CAS"></a>1.1、什么是CAS</h3><p>CAS全称 Central Authentication Service,中央认证服务,一种独立开放指令协议,旨在为web应用系统提供一种可靠的单点登录方法。CAS结构体系分为CAS Server与CAS Client,CAS Server负责用户的认证工作,对用户的用户密码进行校验,生成票据。CAS Client负责处理客户端的访问请求,当需要对请求方进行身份认证时,重定向到CAS Server进行认证。其中有3个核心票据需要我们理解,TGT、TGC、ST。</p><ul><li>TGT(Ticket Grangting Ticket)</li></ul><p>TGT为CAS Server为用户签发的登录票据,当用户在CAS Server登录成功后,CAS Server会为该用户生成唯一TGT对象,表示该用户登录成功,TGT封装了Cookie值以及Cookie值对应的用户信息。CAS Server会将生成的TGT对象放在Session中,同时将生成的Cookie,即TGC,返回给浏览器,TGT对象的ID就是cookie的值。TGC-TGT 相当于key-value,可以根据TGC查询TGT。</p><ul><li>TGC(Ticket Granting Cookie)</li></ul><p>TGC,是存放TGT的Session的唯一标识(SessionId),用以查询TGT</p><ul><li>ST(ServiceTicket)</li></ul><p>ST是CAS Server为用户签发的访问某一服务票据。当用户访问某一Service时,Service发现用户没有ST,就会使用户跳转至CAS Server获取ST,此时,如果用户的请求中含有Cookie,则CAS Server会以这个Cookie值(TGC)为key查询session有无TGT,如果有的话,说明用户已登录,则用此TGT签发一个ST返回给用户。用户携带此票据访问Service,Service拿此ST去CAS Server进行验证,如果验证通过,则允许用户访问资源。<strong>需要注意的是,同一单点登录系统下,多个系统不共用一个ST,CAS Server会为每个系统生成对应ST,但是TGT与TGC都只有一个。</strong></p><h3 id="1-2、认证过程"><a href="#1-2、认证过程" class="headerlink" title="1.2、认证过程"></a>1.2、认证过程</h3><ul><li>登录过程</li></ul><ol><li>用户访问系统一,系统检测用户未登录,携带自己的参数跳转到CAS Server进行认证</li><li>CAS Server发现用户并未登录,返回登录界面</li><li>用户输入用户、密码进行登录</li><li>CAS Server校验用户信息,通过后生成TGT、TGC、ST,将TGT缓存在Session,返回给用户TGC、ST,并重定向到系统一</li><li>用户携带ST访问系统一,系统一请求CAS Server的检验接口对ST进行校验,通过后登录成功,此票据失效</li><li>用户在同域页面携带TGC访问系统二,</li><li>系统检测用户未登录,携带自己的参数跳转到CAS Server进行认证</li><li>CAS Server通过以TGC查询到TGT存在,则用此TGT签发一个ST返回给用户,并重定向到系统二</li><li>用户携带ST访问系统二,系统一请求CAS Server的检验接口对ST进行校验,通过后登录成功,此票据失效</li></ol><ul><li>登出过程</li></ul><ol><li>用户向系统一发出登出请求,系统一携带TGC向CAS Server发起登出请求</li><li>CAS Server根据TGC查询到TGT,销毁TGT信息。同时向所有系统发出登出请求,每个系统的登出接口就是登录接口,只是请求方法不一样,登录是get,登出是post,因此系统需要判断这个接口的请求方法,get就是登录,而post是登出。每一个有效的ticket对应其它系统的一个会话,因此一个ticket会调用一次登出</li><li>各系统接收到登出请求,注销局部会话信息</li><li>CAS Serve重定向到系统一的登录界面,用户需要重新进行登录</li></ol><h3 id="1-3、最佳实践"><a href="#1-3、最佳实践" class="headerlink" title="1.3、最佳实践"></a>1.3、最佳实践</h3><p>网关其实相当于一个cas client,我们以openresty举例子,登录主要过程如下:</p><p>1、客户端发起一个普通请求到openresty,openresty检测到会话无效(过期或者未登录),重定向到cas进行登录。</p><p>2、重定向时,需要组装location,在cas的登录uri后,需要添加service的参数,这个参数的值是openresty本身的地址+登录的uri,这个值非常重要,会有3个作用。</p><p>3、浏览器接收到302的响应会自动跳转,前端其实是不能介入的,这时会引发跨域的问题,需要cas服务端做适配,返回Access-Control-Allow-Origin,但是这也会引发一些问题,HTTP协议具有限制,80以上的浏览器禁止跨域携带cookie。解决的办法很多,架构设计为不跨域的形式或者前端做改造,这里我们采取了前端做改造。</p><p>4、输入用户名和密码cas登录登录成功以后,cas会set-cookie,其中包含TGC,并且会302跳转到service的地址,并在参数携带ticket</p><p>5、这时openresty接收到这个请求,将会去cas校验ticket的真实性,这时也需要组装service,cas校验ticket时也会校验service是否与登录时一致,如果验证通过,则认为该用户已经登录成功。验证时,cas会返回ticket对应的用户,这里其实就有一个高阶用法,不同系统不同用户的映射,可以在这里做设计。</p><p>6、当票据验证成功以后,网关就可以生成会话,同时将会话票据返回到客户端,此时登录成功。</p><p>登出也类似,区别在于cas登出以后,会将登录过的系统都登出,调用的接口就是登录时传递的service,不过此时是post请求。</p><h3 id="1-4、具体分析"><a href="#1-4、具体分析" class="headerlink" title="1.4、具体分析"></a>1.4、具体分析</h3><p>本节具体分析一下1.3中的步骤</p><p>1、首先一个请求触发cas,location会是cas服务端的地址</p><p><img src="/img/cas-01.png" alt="cas-01"></p><p>2、重定向到cas,参数携带service(我们采用另外一种方式避免跨域,所以这里并不是location,只要知道这是前端做了改造即可)</p><p><img src="/img/cas-02.png" alt="cas-02"></p><p>3、此时跳转到cas的登录界面,输入用户和密码以后,进行登录,成功后cas在cookie返回了TGC,并且location跳转了service的地址,里面包含票据</p><p><img src="/img/cas-03.png" alt="cas-03"></p><p>4、接着前端跳转了这个地址(需要注意的是这个请求一定是GET,登出时,cas会调用这个接口,为POST)</p><p><img src="/img/cas-04.png" alt="cas-04"></p><p>此时网关会校验票据,如果登录成功会生成该系统的会话。</p><h3 id="1-5、实际遇到的问题"><a href="#1-5、实际遇到的问题" class="headerlink" title="1.5、实际遇到的问题"></a>1.5、实际遇到的问题</h3><p>要实现一个cas单点登录功能,还是稍微复杂的,大体流程很容易懂,但是具体细节有很多,比如如何解决跨域、service的作用、网关和cas的网络问题、有了域名怎么办、网关前面有代理节点怎么办,如果跳转取host,但是前面的节点不传递host,如何登出等等,这些问题都需要在实践中遇到并解决。</p><h2 id="2、saml"><a href="#2、saml" class="headerlink" title="2、saml"></a>2、saml</h2><p>待补充</p><h2 id="3、oauth-2-0"><a href="#3、oauth-2-0" class="headerlink" title="3、oauth 2.0"></a>3、oauth 2.0</h2><p>待补充</p>]]></content>
<tags>
<tag>SSO</tag>
<tag>cas</tag>
<tag>单点登录</tag>
</tags>
</entry>
<entry>
<title>一次ngx_slab内存泄露问题</title>
<link href="/2024/07/30/%E4%B8%80%E6%AC%A1ngx_slab%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E9%97%AE%E9%A2%98/"/>
<url>/2024/07/30/%E4%B8%80%E6%AC%A1ngx_slab%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E9%97%AE%E9%A2%98/</url>
<content type="html"><![CDATA[<h2 id="1、问题"><a href="#1、问题" class="headerlink" title="1、问题"></a>1、问题</h2><p>根据现场描述,隔几天就会出现nginx无法访问的情况,场景为正常使用状态,出现异常时,日志报大量 ngx_slab_free() : chunk is already free的错误,出现core dump,进程会不端重新拉起,不断core。此情况下,需要重启nginx才能恢复正常。</p><p>现场使用了访问控制功能,其中配置了几百条对客户端ip的访问控制规则。</p><h2 id="2、现象分析"><a href="#2、现象分析" class="headerlink" title="2、现象分析"></a>2、现象分析</h2><p>1、调试core,首先发现core的原因是释放slab内存块时,重复释放了一块内存,ngx_slab_free_locked(shpool,f->k);关键问题是在释放f->k时,没有保护f->k为NULL的场景。但是问题是,为什么会重复释放?其他那个地方已经释放了这块内存,为什么还会走到这块逻辑,或者说这块内存一直为NULL?</p><p>2、调用释放共享内存的函数的地方只有2处</p><ul><li>启动阶段 </li><li>更新访问控制模块的动态配置时</li></ul><p>因此需要着重排查这2块的问题</p><h2 id="3、复现过程"><a href="#3、复现过程" class="headerlink" title="3、复现过程"></a>3、复现过程</h2><p>考虑到现场配置了几百条访问控制规则,于是测试环境配置了1000多条,在重复更新动态配置后,可以复现该问题,nginx出现core,进程会不断被拉起,又崩溃,循环这个过程。在这种异常发生时,nginx不能正常工作,与现场的现象一致。</p><p>可以确认是内存导致的异常,只有重启才能恢复。复现的关键要素是:</p><p>1、访问控制的规则数据量大</p><p>2、重复调用动态配置更新</p><h2 id="4、代码排查"><a href="#4、代码排查" class="headerlink" title="4、代码排查"></a>4、代码排查</h2><h3 id="4-1、初步结论"><a href="#4-1、初步结论" class="headerlink" title="4.1、初步结论"></a>4.1、初步结论</h3><p>nginx是多进程,实现访问控制、限流功能时,进程需要共享数据,比如一个uri的多笔请求,可能由多个进程处理,此时进程需要判断这个uri是否达到了限制次数,因此访问控制模块和限流模块使用共享内存进行进程间的通信,slab共享内存。</p><p>同时,异常时,日志中会出现ngx_slab_alloc() failed:no memory,因此怀疑是共享内存溢出了。目前访问控制模块的slab共享内存大小只有1M,当数据量过大时,1M的内存池会很快用完,后续访问控制模块在处理ip的规则时,会为每一个ip规则申请内存,内存池满时内存申请会失败,但是并不会直接阻断代码逻辑。</p><p>而每次更新配置时,会先清除访问控制模块的内存,因为上次申请内存时没有得到有效的地址,因此在释放时,释放了未知地址,导致core。当work core时,master会尝试拉起一个新的work,初始化访问控制模块时,会初始化共享内存,又会尝试释放上一次的内存,又会导致core,不断循环。</p><p>本质是共享内存用完了,导致模块申请内存时失败,后续又释放了该随机地址,导致core dump。</p><h3 id="4-2、内存泄露"><a href="#4-2、内存泄露" class="headerlink" title="4.2、内存泄露"></a>4.2、内存泄露</h3><p>疑问是,1M的内存,为什么多调用几次就不够用了?所以我怀疑有内存泄漏,如果真的是内存泄露,那么增大多少内存都没有用,因为不重启的情况下,总有一天内存又会满。</p><p>因此需要继续排查,Tengine有一个非常好用的模块,ngx_slab_stat,这个模块的作用与火焰图的ngx-shm类似,可以展示nginx使用的每块共享内存的具体情况(模块地址:<a href="https://www.bookstack.cn/read/nginx-official-doc/29.md%EF%BC%89">https://www.bookstack.cn/read/nginx-official-doc/29.md)</a> 。简单编译使用后,我发现确实有内存泄露,具体测试数据如下:</p><table><thead><tr><th align="center">调用次数/次</th><th align="center">调用接口</th><th align="center">访问控制规则条数/条</th><th align="center">共享内存内存剩余/KB</th></tr></thead><tbody><tr><td align="center">1</td><td align="center">xxx</td><td align="center">823</td><td align="center">896</td></tr><tr><td align="center">2</td><td align="center">xxx</td><td align="center">823</td><td align="center">884</td></tr><tr><td align="center">3</td><td align="center">xxx</td><td align="center">823</td><td align="center">872</td></tr><tr><td align="center">4</td><td align="center">xxx</td><td align="center">823</td><td align="center">860</td></tr><tr><td align="center">5</td><td align="center">xxx</td><td align="center">823</td><td align="center">844</td></tr></tbody></table><p>1、第一次调用</p><p><img src="/img/slab-%E5%9B%BE1.png" alt="slab-图1"></p><p>2、第二次</p><p><img src="/img/slab-%E5%9B%BE2.png" alt="slab-图2"></p><p>3、第三次</p><p><img src="/img/slab-%E5%9B%BE3.png" alt="slab-图3"></p><p>4、第四次</p><p><img src="/img/slab-%E5%9B%BE4.png" alt="slab-图4"></p><p>5、第五次</p><p><img src="/img/slab-%E5%9B%BE5.png" alt="slab-图5"></p><p>823条的访问控制数据,每次会泄露12KB左右的内存,所以内存使用确实是有问题的。</p><h3 id="4-3、代码定位"><a href="#4-3、代码定位" class="headerlink" title="4.3、代码定位"></a>4.3、代码定位</h3><p>slab内存申请主要是2个函数ngx_slab_calloc与ngx_slab_calloc_locked,前者也是调用后者,只不过上了锁。</p><p>访问控制模块申请内存的地方只有3处,但是不确定是哪一处,所以这里取巧一下,这3处申请内存时,分别乘以3、6、9,然后再看泄露的内存是12KB的几倍,实际测试为6倍,因此很快就定位了代码</p><p>最后发现是ip->data申请的内存没有释放,ip是一个ngx_str_t结构体</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> {</span><br><span class="hljs-type">size_t</span> len;<br>u_char *data;<br>} <span class="hljs-type">ngx_str_t</span>;<br></code></pre></td></tr></table></figure><p>ip本身和data都申请了内存</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">ngx_str_t</span> *ip;<br>ip = ngx_slab_calloc_locked(xxxxx,<span class="hljs-keyword">sizeof</span>(<span class="hljs-type">ngx_str_t</span>));<br>ip->data = ngx_slab_calloc_locked(xxxxx,xxxxxx);<br></code></pre></td></tr></table></figure><p>但是释放时,只释放了ip本身,并没有释放ip->data,因此每次都会有内存泄露。</p><p>因此修复代码缺陷,正确释放ip->data,再进行测试,发现没有了内存泄露问题,可用内存一直处于固定值。</p><h2 id="5、总结"><a href="#5、总结" class="headerlink" title="5、总结"></a>5、总结</h2><p>这个问题很有意思,借着这次的问题对slab有了一些了解,看了陶辉的《深入理解Nginx模块开发与架构解析》第16章感觉受益匪浅,然后简单看了下linux关于slab的原理,整体有一定的理解,具体总结在另外一篇文章,会持续更新。</p><p>这次还使用了火焰图辅助排查,章亦春的2个脚本,不过只有ngx-shm执行成功了,不过效果不如Tengine的ngx_slab_stat,这个确实很好很好的模块,后续我会编译到我们的产品中去,而且我发现Tengine还有一些有意思的模块,后续可以研究一下。</p><p>其实看了陶辉的《深入理解Nginx模块开发与架构解析》第16章,ngx_slab_stat的实现原理就很容易理解,后续有时间可以写一篇分析文章。</p><p>最后找到问题,其实发现是c语言指针、内存的问题,c语言确实容易出现这个问题。</p>]]></content>
<tags>
<tag>nginx,slab</tag>
</tags>
</entry>
<entry>
<title>ngx_slab共享内存</title>
<link href="/2024/07/09/ngx_slab/"/>
<url>/2024/07/09/ngx_slab/</url>
<content type="html"><![CDATA[<p>nginx是多进程,实现访问控制、限流功能时,进程需要共享数据,比如一个uri的多笔请求,可能由多个进程处理,此时进程需要判断这个uri是否达到了限制次数,因此访问控制模块和限流模块使用共享内存进行进程间的通信,nginx实现了ngx_shm_t共享内存,但是如果要共享一些复杂的数据结构,ngx_shm_t很难满足这种需求,因此在这个基础上实现了slab共享内存。</p><h2 id="1、初始化共享内存"><a href="#1、初始化共享内存" class="headerlink" title="1、初始化共享内存"></a>1、初始化共享内存</h2><p>模块在配置初始化时,将会申请一块slab内存池,开发者可以通过ngx_slab_alloc向这个内存池申请内存,当内存池用尽时,这个函数就会返回NULL。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">ngx_shm_zone_t</span> *<br><br><span class="hljs-title function_">ngx_shared_memory_add</span><span class="hljs-params">(<span class="hljs-type">ngx_conf_t</span> *cf, <span class="hljs-type">ngx_str_t</span> *name, <span class="hljs-type">size_t</span> size, <span class="hljs-type">void</span>* *tag)</span><br></code></pre></td></tr></table></figure><ul><li>ngx_conf_t *cf //全局配置文件</li><li>ngx_str_t *name //这块slab共享内存的名字</li><li>size_t size //这块共享内存的大小</li><li>void *tag //防止2个不同模块定义的内存池具有相同的名字,一般传入本模块结构体的地址</li></ul><p>本模块结构体的地址通常为全局变量,因此在reload,nginx重读配置时,因为tag没有变化,所以不会重新申请内存。还有一个好处是,如果之前共享内存是有数据的,这样不会丢掉之前共享内存中的数据,因此使用的思想是,尽可能使用旧的共享内存,当然前提是旧的存在。</p><h2 id="2、操作slab共享内存"><a href="#2、操作slab共享内存" class="headerlink" title="2、操作slab共享内存"></a>2、操作slab共享内存</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">//初始化共享内存</span><br><span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_slab_init</span><span class="hljs-params">(<span class="hljs-type">ngx_slab_pool_t</span> *pool)</span><br><br><span class="hljs-comment">//加锁的内存申请方法</span><br><span class="hljs-type">void</span> *<br><span class="hljs-title function_">ngx_slab_alloc</span><span class="hljs-params">(<span class="hljs-type">ngx_slab_pool_t</span> *pool, <span class="hljs-type">size_t</span> size)</span><br><br><span class="hljs-comment">//不加锁的内存申请方法</span><br><span class="hljs-type">void</span> *<br><span class="hljs-title function_">ngx_slab_alloc_locked</span><span class="hljs-params">(<span class="hljs-type">ngx_slab_pool_t</span> *pool, <span class="hljs-type">size_t</span> size)</span><br><br><span class="hljs-comment">//加锁的内存释放方法</span><br><span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_slab_free</span><span class="hljs-params">(<span class="hljs-type">ngx_slab_pool_t</span> *pool, <span class="hljs-type">void</span> *p)</span><br><br><span class="hljs-comment">//不加锁的内存释放方法</span><br><span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_slab_free_locked</span><span class="hljs-params">(<span class="hljs-type">ngx_slab_pool_t</span> *pool, <span class="hljs-type">void</span> *p)</span><br> <br><br></code></pre></td></tr></table></figure><p>nginx多进程结构,需要使用同步锁才能操作共享数据。那为什么还有ngx_slab_alloc_locked?事实上,nginx的代码可能存在多层锁的嵌套,如果外层已经加锁,那么内存是没有必要上锁的,毕竟上锁会增加开销,降低效率。</p><p>需要注意的是,当slab内存池的内存用完时,ngx_slab_alloc会直接返回NULL,因此需要合理评估模块使用的内存大小,如果slab共享内存设置的太小会导致异常。</p><p>以ssl模块为例,共享内存</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c">sscf->shm_zone = ngx_shared_memory_add(cf, &name, n,<br> &ngx_http_ssl_module);<br></code></pre></td></tr></table></figure><h2 id="3、API详解"><a href="#3、API详解" class="headerlink" title="3、API详解"></a>3、API详解</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">void</span> <span class="hljs-title function_">ngx_slab_free_locked</span><span class="hljs-params">(<span class="hljs-type">ngx_slab_pool_t</span> *pool, <span class="hljs-type">void</span> *p)</span><br>{<br> <span class="hljs-comment">// 定义局部变量,用于计算和存储内存页和slab信息。</span><br> <span class="hljs-type">size_t</span> size;<br> <span class="hljs-type">uintptr_t</span> slab, m, *bitmap;<br> <span class="hljs-type">ngx_uint_t</span> i, n, type, slot, shift, <span class="hljs-built_in">map</span>;<br> <span class="hljs-type">ngx_slab_page_t</span> *slots, *page;<br><br> <span class="hljs-comment">// 记录调试信息,显示正在释放的内存地址。</span><br> ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, ngx_cycle-><span class="hljs-built_in">log</span>, <span class="hljs-number">0</span>, <span class="hljs-string">"slab free: %p"</span>, p);<br><br> <span class="hljs-comment">// 检查释放的内存地址是否在slab pool的范围内。</span><br> <span class="hljs-keyword">if</span> ((u_char *) p < pool->start || (u_char *) p > pool->end) {<br> <span class="hljs-comment">// 如果不在范围内,记录错误日志并退出函数。</span><br> ngx_slab_error(pool, NGX_LOG_ALERT, <span class="hljs-string">"ngx_slab_free(): outside of pool"</span>);<br> <span class="hljs-keyword">goto</span> fail;<br> }<br><br> <span class="hljs-comment">// 计算内存页的索引。</span><br> n = ((u_char *) p - pool->start) >> ngx_pagesize_shift;<br> <span class="hljs-comment">// 获取内存页的指针。</span><br> page = &pool->pages[n];<br> <span class="hljs-comment">// 获取slab的地址。</span><br> slab = page->slab;<br> <span class="hljs-comment">// 获取内存页的类型。</span><br> type = ngx_slab_page_type(page);<br><br> <span class="hljs-comment">// 根据内存页类型进行不同的处理。</span><br> <span class="hljs-keyword">switch</span> (type) {<br> <span class="hljs-comment">// 小对象内存页的处理。</span><br> <span class="hljs-keyword">case</span> NGX_SLAB_SMALL:<br> <span class="hljs-comment">// 计算slab的大小和位移。</span><br> shift = slab & NGX_SLAB_SHIFT_MASK;<br> size = (<span class="hljs-type">size_t</span>) <span class="hljs-number">1</span> << shift;<br><br> <span class="hljs-comment">// 检查p是否是size的整数倍。</span><br> <span class="hljs-keyword">if</span> ((<span class="hljs-type">uintptr_t</span>) p & (size - <span class="hljs-number">1</span>)) {<br> <span class="hljs-keyword">goto</span> wrong_chunk;<br> }<br><br> <span class="hljs-comment">// 计算在bitmap中的位置。</span><br> n = ((<span class="hljs-type">uintptr_t</span>) p & (ngx_pagesize - <span class="hljs-number">1</span>)) >> shift;<br> m = (<span class="hljs-type">uintptr_t</span>) <span class="hljs-number">1</span> << (n % (<span class="hljs-number">8</span> * <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">uintptr_t</span>)));<br> n /= <span class="hljs-number">8</span> * <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">uintptr_t</span>);<br> bitmap = (<span class="hljs-type">uintptr_t</span> *)((<span class="hljs-type">uintptr_t</span>) p & ~((<span class="hljs-type">uintptr_t</span>) ngx_pagesize - <span class="hljs-number">1</span>));<br><br> <span class="hljs-comment">// 检查bitmap对应的位是否被设置,即内存块是否已被分配。</span><br> <span class="hljs-keyword">if</span> (bitmap[n] & m) {<br> <span class="hljs-comment">// 释放内存块,更新slab的bitmap和内存页的链表。</span><br> <span class="hljs-comment">// ...</span><br> <span class="hljs-comment">// 省略了释放内存块的代码。</span><br> }<br><br> <span class="hljs-comment">// 如果内存块已经被释放,则报错。</span><br> <span class="hljs-keyword">goto</span> chunk_already_free;<br><br> <span class="hljs-comment">// 精确大小内存页的处理。</span><br> <span class="hljs-keyword">case</span> NGX_SLAB_EXACT:<br> <span class="hljs-comment">// ...</span><br> <span class="hljs-comment">// 省略了精确大小内存页的处理代码。</span><br><br> <span class="hljs-comment">// 大对象内存页的处理。</span><br> <span class="hljs-keyword">case</span> NGX_SLAB_BIG:<br> <span class="hljs-comment">// ...</span><br> <span class="hljs-comment">// 省略了大对象内存页的处理代码。</span><br><br> <span class="hljs-comment">// 特殊内存页的处理,用于存储大于slab可以分配的最大块大小的对象。</span><br> <span class="hljs-keyword">case</span> NGX_SLAB_PAGE:<br> <span class="hljs-comment">// ...</span><br> <span class="hljs-comment">// 省略了特殊内存页的处理代码。</span><br><br> <span class="hljs-keyword">default</span>:<br> <span class="hljs-comment">// 未处理的case,不应该到达这里。</span><br> <span class="hljs-keyword">break</span>;<br> }<br><br> <span class="hljs-comment">// 函数结束,正常释放内存后会执行到这里。</span><br> <span class="hljs-keyword">return</span>;<br><br>fail:<br> <span class="hljs-comment">// 释放失败,记录错误日志。</span><br> <span class="hljs-keyword">return</span>;<br><br>done:<br> <span class="hljs-comment">// 正常释放内存后更新使用的统计信息,并填充释放的内存以避免重复使用。</span><br> pool->stats[slot].used--;<br> ngx_slab_junk(p, size);<br> <span class="hljs-keyword">return</span>;<br><br>wrong_chunk:<br> <span class="hljs-comment">// 释放的内存块地址不正确,记录错误日志。</span><br> ngx_slab_error(pool, NGX_LOG_ALERT, <span class="hljs-string">"ngx_slab_free(): pointer to wrong chunk"</span>);<br> <span class="hljs-keyword">goto</span> fail;<br><br>chunk_already_free:<br> <span class="hljs-comment">// 尝试释放一个已经被释放的内存块,记录错误日志。</span><br> ngx_slab_error(pool, NGX_LOG_ALERT, <span class="hljs-string">"ngx_slab_free(): chunk is already free"</span>);<br> <span class="hljs-keyword">goto</span> fail;<br>}<br></code></pre></td></tr></table></figure><h2 id="4、释放问题"><a href="#4、释放问题" class="headerlink" title="4、释放问题"></a>4、释放问题</h2><p>ngx_slab_free_locked 函数释放通过 slab 分配器分配的内存时,不会改变指针本身的值,而是将指针指向的内存块标记为可用。这个很关键,因此当一个指针是否持有合理内存时,不能判断是否为NULL。</p><p>内存分配器(如 slab 分配器)负责管理内存块的分配和释放。当一个内存块被分配时,内存分配器会记录该内存块的状态(已分配)。当这个内存块被释放时,内存分配器会更新该内存块的状态(可用)。</p><p>在 C 语言中,指针是用来存储内存地址的变量。指针本身只是一个变量,存储了一个内存地址。在调用 ngx_slab_free_locked 函数时,传递的是指针的值(即内存地址),而不是指针本身。因此,函数内部对内存的操作不会改变传入指针的值。</p><p>指针传递: 当调用 ngx_slab_free_locked(shpool, h) 时,传递的是 h 的值(内存地址)。<br>内存释放: 函数 ngx_slab_free_locked 使用 h 指向的内存地址,在内存池中找到对应的内存块,并将其标记为可用。这涉及到更新 slab 分配器内部的数据结构,但不改变 h 本身的值。</p><p><strong>指针保持不变: 函数调用结束后,h 仍然持有原来的内存地址值。</strong></p>]]></content>
<tags>
<tag>nginx,slab</tag>
</tags>
</entry>
<entry>
<title>负载均衡算法解析</title>
<link href="/2024/06/02/Nginx%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E7%AE%97%E6%B3%95%E8%A7%A3%E6%9E%90/"/>
<url>/2024/06/02/Nginx%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E7%AE%97%E6%B3%95%E8%A7%A3%E6%9E%90/</url>
<content type="html"><![CDATA[<h2 id="1、nginx-轮询"><a href="#1、nginx-轮询" class="headerlink" title="1、nginx-轮询"></a>1、nginx-轮询</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">ngx_http_upstream_rr_peer_t</span> *<br><span class="hljs-title function_">ngx_http_upstream_get_peer</span><span class="hljs-params">(<span class="hljs-type">ngx_http_upstream_rr_peer_data_t</span> *rrp)</span><br>{<br> <span class="hljs-type">time_t</span> now; <span class="hljs-comment">// 当前时间</span><br> <span class="hljs-type">uintptr_t</span> m; <span class="hljs-comment">// 位掩码</span><br> <span class="hljs-type">ngx_int_t</span> total; <span class="hljs-comment">// 总权重</span><br> <span class="hljs-type">ngx_uint_t</span> i, n, p; <span class="hljs-comment">// 循环计数器和索引</span><br> <span class="hljs-type">ngx_http_upstream_rr_peer_t</span> *peer, *best; <span class="hljs-comment">// 指向当前和最佳服务器的指针</span><br><br> now = ngx_time();<br><br> <span class="hljs-comment">// 初始化最佳服务器为 NULL 和总权重为 0</span><br> best = <span class="hljs-literal">NULL</span>;<br> total = <span class="hljs-number">0</span>;<br><br> <span class="hljs-comment">// 避免编译器警告,如果未使用变量 p</span><br><span class="hljs-meta">#<span class="hljs-keyword">if</span> (NGX_SUPPRESS_WARN)</span><br> p = <span class="hljs-number">0</span>;<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br><br> <span class="hljs-comment">// 遍历所有后端服务器</span><br> <span class="hljs-keyword">for</span> (peer = rrp->peers->peer, i = <span class="hljs-number">0</span>;<br> peer;<br> peer = peer->next, i++)<br> {<br> <span class="hljs-comment">// 计算位数组索引和位掩码</span><br> n = i / (<span class="hljs-number">8</span> * <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">uintptr_t</span>)); <span class="hljs-comment">// 索引为当前服务器编号除以每个 uintptr_t 能存储的位数</span><br> m = (<span class="hljs-type">uintptr_t</span>) <span class="hljs-number">1</span> << i % (<span class="hljs-number">8</span> * <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">uintptr_t</span>)); <span class="hljs-comment">// 计算位掩码</span><br><br> <span class="hljs-comment">// 如果当前服务器已经被尝试过,则跳过</span><br> <span class="hljs-keyword">if</span> (rrp->tried[n] & m) {<br> <span class="hljs-keyword">continue</span>;<br> }<br><br> <span class="hljs-comment">// 如果服务器处于宕机状态,则跳过</span><br> <span class="hljs-keyword">if</span> (peer->down) {<br> <span class="hljs-keyword">continue</span>;<br> }<br><br> <span class="hljs-comment">// 如果服务器的失败次数超过了允许的最大失败次数,并且当前时间距离上次检查时间小于失败超时时间,则跳过</span><br> <span class="hljs-keyword">if</span> (peer->max_fails<br> && peer->fails >= peer->max_fails<br> && now - peer->checked <= peer->fail_timeout)<br> {<br> <span class="hljs-keyword">continue</span>;<br> }<br><br> <span class="hljs-comment">// 如果服务器达到最大连接数限制,则跳过</span><br> <span class="hljs-keyword">if</span> (peer->max_conns && peer->conns >= peer->max_conns) {<br> <span class="hljs-keyword">continue</span>;<br> }<br><br> <span class="hljs-comment">// 增加服务器的当前权重,并将其加入总权重</span><br> peer->current_weight += peer->effective_weight;<br> total += peer->effective_weight;<br><br> <span class="hljs-comment">// 如果服务器的有效权重小于其声明的权重,则增加有效权重</span><br> <span class="hljs-keyword">if</span> (peer->effective_weight < peer->weight) {<br> peer->effective_weight++;<br> }<br><br> <span class="hljs-comment">// 选择当前权重最高的服务器作为最佳服务器</span><br> <span class="hljs-keyword">if</span> (best == <span class="hljs-literal">NULL</span> || peer->current_weight > best->current_weight) {<br> best = peer;<br> p = i; <span class="hljs-comment">// 记录最佳服务器的索引</span><br> }<br> }<br><br> <span class="hljs-comment">// 如果没有找到合适的服务器,返回 NULL</span><br> <span class="hljs-keyword">if</span> (best == <span class="hljs-literal">NULL</span>) {<br> <span class="hljs-keyword">return</span> <span class="hljs-literal">NULL</span>;<br> }<br><br> <span class="hljs-comment">// 设置当前选择的服务器</span><br> rrp->current = best;<br><br> <span class="hljs-comment">// 更新位数组以记录已尝试的服务器</span><br> n = p / (<span class="hljs-number">8</span> * <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">uintptr_t</span>));<br> m = (<span class="hljs-type">uintptr_t</span>) <span class="hljs-number">1</span> << p % (<span class="hljs-number">8</span> * <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">uintptr_t</span>));<br> rrp->tried[n] |= m;<br><br> <span class="hljs-comment">// 从最佳服务器的当前权重中减去总权重,为下一次选择做准备</span><br> best->current_weight -= total;<br><br> <span class="hljs-comment">// 如果当前时间距离最佳服务器的上次检查时间超过失败超时时间,则更新检查时间</span><br> <span class="hljs-keyword">if</span> (now - best->checked > best->fail_timeout) {<br> best->checked = now;<br> }<br><br> <span class="hljs-comment">// 返回选择的最佳服务器</span><br> <span class="hljs-keyword">return</span> best;<br>}<br></code></pre></td></tr></table></figure><h2 id="2、nginx-ip-hash"><a href="#2、nginx-ip-hash" class="headerlink" title="2、nginx-ip_hash"></a>2、nginx-ip_hash</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs c">upstream rrBackend {<br> ip_hash;<br> server localhost:<span class="hljs-number">8001</span> weight=<span class="hljs-number">1</span>;<br> server localhost:<span class="hljs-number">8002</span> weight=<span class="hljs-number">2</span>;<br> server localhost:<span class="hljs-number">8003</span> weight=<span class="hljs-number">3</span>;<br>}<br><br>location /rr {<br> proxy_pass http:<span class="hljs-comment">//rrBackend;</span><br>}<br></code></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br></pre></td><td class="code"><pre><code class="hljs c">ngx_http_upstream_get_ip_hash_peer(<span class="hljs-type">ngx_peer_connection_t</span> *pc, <span class="hljs-type">void</span> *data)<br>{<br> <span class="hljs-type">ngx_http_upstream_ip_hash_peer_data_t</span> *iphp = data;<br><br> <span class="hljs-type">time_t</span> now;<br> <span class="hljs-type">ngx_int_t</span> w;<br> <span class="hljs-type">uintptr_t</span> m;<br> <span class="hljs-type">ngx_uint_t</span> i, n, p, hash;<br> <span class="hljs-type">ngx_http_upstream_rr_peer_t</span> *peer;<br><br> ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc-><span class="hljs-built_in">log</span>, <span class="hljs-number">0</span>,<br> <span class="hljs-string">"get ip hash peer, try: %ui"</span>, pc->tries);<br><br> <span class="hljs-comment">// 对轮询节点的peers进行读锁定</span><br> ngx_http_upstream_rr_peers_rlock(iphp->rrp.peers);<br><br> <span class="hljs-comment">// 如果尝试次数超过20次或者只有一个后端节点,则直接返回轮询算法的结果</span><br> <span class="hljs-keyword">if</span> (iphp->tries > <span class="hljs-number">20</span> || iphp->rrp.peers->single) {<br> ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers);<br> <span class="hljs-keyword">return</span> iphp->get_rr_peer(pc, &iphp->rrp);<br> }<br><br> now = ngx_time();<br><br> pc->cached = <span class="hljs-number">0</span>;<br> pc->connection = <span class="hljs-literal">NULL</span>;<br><br> hash = iphp->hash;<br><br> <span class="hljs-keyword">for</span> ( ;; ) {<br><br> <span class="hljs-comment">// 计算哈希值,这里只取地址的前三位</span><br> <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i < (<span class="hljs-type">ngx_uint_t</span>) iphp->addrlen; i++) {<br> hash = (hash * <span class="hljs-number">113</span> + iphp->addr[i]) % <span class="hljs-number">6271</span>;<br> }<br><br> <span class="hljs-comment">// 对总权重取余,使得请求更加均匀的分散到server</span><br> w = hash % iphp->rrp.peers->total_weight;<br> peer = iphp->rrp.peers->peer;<br> p = <span class="hljs-number">0</span>;<br><br> <span class="hljs-comment">// 遍历peers,找到权重匹配的peer,权重值越大,越容易得到这个请求</span><br> <span class="hljs-keyword">while</span> (w >= peer->weight) {<br> w -= peer->weight;<br> peer = peer->next;<br> p++;<br> }<br><br> <span class="hljs-comment">// 检查这个peer是否被尝试过</span><br> n = p / (<span class="hljs-number">8</span> * <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">uintptr_t</span>));<br> m = (<span class="hljs-type">uintptr_t</span>) <span class="hljs-number">1</span> << p % (<span class="hljs-number">8</span> * <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">uintptr_t</span>));<br><br> <span class="hljs-comment">// 如果已经尝试过这个peer,则跳过</span><br> <span class="hljs-keyword">if</span> (iphp->rrp.tried[n] & m) {<br> <span class="hljs-keyword">goto</span> next;<br> }<br><br> ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pc-><span class="hljs-built_in">log</span>, <span class="hljs-number">0</span>,<br> <span class="hljs-string">"get ip hash peer, hash: %ui %04XL"</span>, p, (<span class="hljs-type">uint64_t</span>) m);<br><br> <span class="hljs-comment">// 对选定的peer进行加锁</span><br> ngx_http_upstream_rr_peer_lock(iphp->rrp.peers, peer);<br><br> <span class="hljs-comment">// 如果peer处于down状态,则解锁并跳过</span><br> <span class="hljs-keyword">if</span> (peer->down) {<br> ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);<br> <span class="hljs-keyword">goto</span> next;<br> }<br><br> <span class="hljs-comment">// 如果peer失败次数超过阈值并且检查时间在失败超时时间内,则解锁并跳过</span><br> <span class="hljs-keyword">if</span> (peer->max_fails<br> && peer->fails >= peer->max_fails<br> && now - peer->checked <= peer->fail_timeout)<br> {<br> ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);<br> <span class="hljs-keyword">goto</span> next;<br> }<br><br> <span class="hljs-comment">// 如果peer的连接数超过最大连接数限制,则解锁并跳过</span><br> <span class="hljs-keyword">if</span> (peer->max_conns && peer->conns >= peer->max_conns) {<br> ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);<br> <span class="hljs-keyword">goto</span> next;<br> }<br><br> <span class="hljs-comment">// 如果没有上述情况,则选择此peer</span><br> <span class="hljs-keyword">break</span>;<br><br> next:<br><br> <span class="hljs-comment">// 如果尝试次数超过20次,则返回轮询算法的结果</span><br> <span class="hljs-keyword">if</span> (++iphp->tries > <span class="hljs-number">20</span>) {<br> ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers);<br> <span class="hljs-keyword">return</span> iphp->get_rr_peer(pc, &iphp->rrp);<br> }<br> }<br><br> <span class="hljs-comment">// 设置当前选择的peer</span><br> iphp->rrp.current = peer;<br><br> <span class="hljs-comment">// 设置pc结构体的字段,以反映选定的peer</span><br> pc->sockaddr = peer->sockaddr;<br> pc->socklen = peer->socklen;<br> pc->name = &peer->name;<br><br> <span class="hljs-comment">// 增加peer的连接数</span><br> peer->conns++;<br><br> <span class="hljs-comment">// 更新peer的检查时间</span><br> <span class="hljs-keyword">if</span> (now - peer->checked > peer->fail_timeout) {<br> peer->checked = now;<br> }<br><br> <span class="hljs-comment">// 解锁选定的peer</span><br> ngx_http_upstream_rr_peer_unlock(iphp->rrp.peers, peer);<br> ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers);<br><br> <span class="hljs-comment">// 在trie中标记已经尝试过这个peer</span><br> iphp->rrp.tried[n] |= m;<br> iphp->hash = hash;<br><br> <span class="hljs-comment">// 函数返回成功</span><br> <span class="hljs-keyword">return</span> NGX_OK;<br>}<br></code></pre></td></tr></table></figure><h2 id="3、hash"><a href="#3、hash" class="headerlink" title="3、hash"></a>3、hash</h2><p>使用:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs javascript">upstream myapp {<br> hash $http_x_real_ip; # 使用 X-<span class="hljs-title class_">Real</span>-<span class="hljs-variable constant_">IP</span> 头部的值进行哈希<br> server backend1.<span class="hljs-property">example</span>.<span class="hljs-property">com</span> weight=<span class="hljs-number">1</span>;<br> server backend2.<span class="hljs-property">example</span>.<span class="hljs-property">com</span> weight=<span class="hljs-number">2</span>;<br> }<br><br><span class="hljs-comment">//或者</span><br><br>upstream myapp {<br> hash $http_x_forwarded_for; # 使用 $http_x_forwarded_for 头部的值进行哈希<br> server backend1.<span class="hljs-property">example</span>.<span class="hljs-property">com</span> weight=<span class="hljs-number">1</span>;<br> server backend2.<span class="hljs-property">example</span>.<span class="hljs-property">com</span> weight=<span class="hljs-number">2</span>;<br> }<br></code></pre></td></tr></table></figure><p>当在 Nginx 配置中使用 <code>hash $http_x_forwarded_for;</code> 作为负载均衡的键时,hash用的就是真实客户端ip,<code>ngx_http_complex_value</code> 函数将用于计算 <code>X-Forwarded-For</code> HTTP 请求头的值,并将该值赋给 <code>hp->key</code>。以下是该过程的详细说明:</p><p><img src="/img/%E5%9B%BE3.1.png" alt="图3.1"></p><ol><li><strong>复杂值初始化</strong>:<ul><li>在 Nginx 配置阶段,当遇到 <code>hash $http_x_forwarded_for;</code> 配置时,相关的配置处理函数(如 <code>ngx_http_upstream_hash</code>)将初始化一个 <code>ngx_http_complex_value_t</code> 结构体,这里即 <code>hcf->key</code>。</li></ul></li><li><strong>请求处理阶段</strong>:<ul><li>当一个请求到达并需要进行上游处理时,<code>ngx_http_upstream_init_hash_peer</code> 函数被调用。</li></ul></li><li><strong>执行复杂值</strong>:<ul><li><code>ngx_http_complex_value</code> 函数被用来执行 <code>hcf->key</code> 中定义的复杂值,这个复杂值就是 <code>$http_x_forwarded_for</code>。</li></ul></li><li><strong>计算哈希键</strong>:<ul><li><code>ngx_http_complex_value</code> 函数解析 <code>$http_x_forwarded_for</code>,这通常意味着它将获取请求的 <code>X-Forwarded-For</code> 头的值。</li></ul></li><li><strong>值的变化</strong>:<ul><li>在执行 <code>ngx_http_complex_value(r, &hcf->key, &hp->key)</code> 之前,<code>hp->key</code> 是未初始化的。</li><li>执行后,如果函数返回 <code>NGX_OK</code>,则 <code>hp->key</code> 将包含 <code>X-Forwarded-For</code> 头的值,这可能是一个单一的 IP 地址或者一个 IP 地址列表,具体取决于 <code>X-Forwarded-For</code> 头的内容。</li></ul></li><li><strong>错误处理</strong>:<ul><li>如果 <code>ngx_http_complex_value</code> 函数返回 <code>NGX_ERROR</code>,这通常意味着在尝试获取或计算 <code>X-Forwarded-For</code> 头的值时出现了问题,比如内存分配失败。在这种情况下,<code>ngx_http_upstream_init_hash_peer</code> 函数将返回 <code>NGX_ERROR</code>,导致当前请求的上游处理初始化失败。</li></ul></li><li><strong>调试日志</strong>:<ul><li>如果 <code>ngx_http_complex_value</code> 成功执行,将记录一条调试日志,显示 “upstream hash key” 以及计算出的键值。</li></ul></li><li><strong>继续处理</strong>:<ul><li>如果 <code>ngx_http_complex_value</code> 成功,函数将继续执行,<code>hp->key</code> 将用于后续的哈希计算和对等体选择过程。</li></ul></li></ol><p>总结来说,<code>if (ngx_http_complex_value(r, &hcf->key, &hp->key) != NGX_OK) { return NGX_ERROR; }</code> 这段代码是用来检查 <code>ngx_http_complex_value</code> 函数是否成功执行,并根据执行结果决定是否继续处理请求。如果 <code>X-Forwarded-For</code> 头存在且格式正确,<code>hp->key</code> 将被赋予相应的值;如果获取头信息失败或在执行过程中遇到错误,请求处理将被中止,并返回错误状态。</p><p>初始化:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs c">tatic <span class="hljs-type">ngx_command_t</span> ngx_http_upstream_hash_commands[] = {<br><br> { ngx_string(<span class="hljs-string">"hash"</span>),<br> NGX_HTTP_UPS_CONF|NGX_CONF_TAKE12,<br> ngx_http_upstream_hash,<br> NGX_HTTP_SRV_CONF_OFFSET,<br> <span class="hljs-number">0</span>,<br> <span class="hljs-literal">NULL</span> },<br><br> ngx_null_command<br>};<br></code></pre></td></tr></table></figure><p>设置值:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">char</span> *<br><span class="hljs-title function_">ngx_http_upstream_hash</span><span class="hljs-params">(<span class="hljs-type">ngx_conf_t</span> *cf, <span class="hljs-type">ngx_command_t</span> *cmd, <span class="hljs-type">void</span> *conf)</span><br>{<br> <span class="hljs-type">ngx_http_upstream_hash_srv_conf_t</span> *hcf = conf;<br><br> <span class="hljs-type">ngx_str_t</span> *value;<br> <span class="hljs-type">ngx_http_upstream_srv_conf_t</span> *uscf;<br> <span class="hljs-type">ngx_http_compile_complex_value_t</span> ccv;<br><br> value = cf->args->elts;<br><br> ngx_memzero(&ccv, <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">ngx_http_compile_complex_value_t</span>));<br><br> ccv.cf = cf;<br> ccv.value = &value[<span class="hljs-number">1</span>];<br> ccv.complex_value = &hcf->key;<br><br> <span class="hljs-keyword">if</span> (ngx_http_compile_complex_value(&ccv) != NGX_OK) { <span class="hljs-comment">//变量替换值,比如:hash $http_x_forwarded_for;</span><br> <span class="hljs-keyword">return</span> NGX_CONF_ERROR;<br> }<br><br> uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);<br><br> <span class="hljs-keyword">if</span> (uscf->peer.init_upstream) {<br> ngx_conf_log_error(NGX_LOG_WARN, cf, <span class="hljs-number">0</span>,<br> <span class="hljs-string">"load balancing method redefined"</span>);<br> }<br><br> uscf->flags = NGX_HTTP_UPSTREAM_CREATE<br> |NGX_HTTP_UPSTREAM_WEIGHT<br> |NGX_HTTP_UPSTREAM_MAX_CONNS<br> |NGX_HTTP_UPSTREAM_MAX_FAILS<br> |NGX_HTTP_UPSTREAM_FAIL_TIMEOUT<br> |NGX_HTTP_UPSTREAM_DOWN;<br><br> <span class="hljs-keyword">if</span> (cf->args->nelts == <span class="hljs-number">2</span>) {<br> uscf->peer.init_upstream = ngx_http_upstream_init_hash;<br><br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (ngx_strcmp(value[<span class="hljs-number">2</span>].data, <span class="hljs-string">"consistent"</span>) == <span class="hljs-number">0</span>) {<br> uscf->peer.init_upstream = ngx_http_upstream_init_chash;<br><br> } <span class="hljs-keyword">else</span> {<br> ngx_conf_log_error(NGX_LOG_EMERG, cf, <span class="hljs-number">0</span>,<br> <span class="hljs-string">"invalid parameter \"%V\""</span>, &value[<span class="hljs-number">2</span>]);<br> <span class="hljs-keyword">return</span> NGX_CONF_ERROR;<br> }<br><br> <span class="hljs-keyword">return</span> NGX_CONF_OK;<br>}<br><br></code></pre></td></tr></table></figure><p>应用:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">ngx_int_t</span><br><span class="hljs-title function_">ngx_http_upstream_init_hash_peer</span><span class="hljs-params">(<span class="hljs-type">ngx_http_request_t</span> *r,</span><br><span class="hljs-params"> <span class="hljs-type">ngx_http_upstream_srv_conf_t</span> *us)</span><br>{<br> <span class="hljs-type">ngx_http_upstream_hash_srv_conf_t</span> *hcf;<br> <span class="hljs-type">ngx_http_upstream_hash_peer_data_t</span> *hp;<br><br> hp = ngx_palloc(r->pool, <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">ngx_http_upstream_hash_peer_data_t</span>));<br> <span class="hljs-keyword">if</span> (hp == <span class="hljs-literal">NULL</span>) {<br> <span class="hljs-keyword">return</span> NGX_ERROR;<br> }<br><br> r->upstream->peer.data = &hp->rrp;<br><br> <span class="hljs-keyword">if</span> (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) {<br> <span class="hljs-keyword">return</span> NGX_ERROR;<br> }<br><br> r->upstream->peer.get = ngx_http_upstream_get_hash_peer;<br><br> hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);<br><br> <span class="hljs-keyword">if</span> (ngx_http_complex_value(r, &hcf->key, &hp->key) != NGX_OK) {<br> <span class="hljs-keyword">return</span> NGX_ERROR;<br> }<br><br> ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection-><span class="hljs-built_in">log</span>, <span class="hljs-number">0</span>, <span class="hljs-comment">//这里日志会输出</span><br> <span class="hljs-string">"upstream hash key:\"%V\""</span>, &hp->key);<br><br> hp->conf = hcf;<br> hp->tries = <span class="hljs-number">0</span>;<br> hp->rehash = <span class="hljs-number">0</span>;<br> hp->hash = <span class="hljs-number">0</span>;<br> hp->get_rr_peer = ngx_http_upstream_get_round_robin_peer;<br><br> <span class="hljs-keyword">return</span> NGX_OK;<br>}<br></code></pre></td></tr></table></figure><h2 id="4、sticky"><a href="#4、sticky" class="headerlink" title="4、sticky"></a>4、sticky</h2><p>参考自:陶辉《深入剖析Nginx负载均衡算法》:<a href="https://www.nginx.org.cn/article/detail/440">https://www.nginx.org.cn/article/detail/440</a></p>]]></content>
<tags>
<tag>负载均衡</tag>
<tag>nginx</tag>
</tags>
</entry>
<entry>
<title>浅析nginx实现websocket原理</title>
<link href="/2024/04/12/%E6%B5%85%E6%9E%90nginx%E5%AE%9E%E7%8E%B0websocket%E5%8E%9F%E7%90%86/"/>
<url>/2024/04/12/%E6%B5%85%E6%9E%90nginx%E5%AE%9E%E7%8E%B0websocket%E5%8E%9F%E7%90%86/</url>
<content type="html"><![CDATA[<h2 id="前言:"><a href="#前言:" class="headerlink" title="前言:"></a>前言:</h2><p>传统的HTTP协议是一种无状态的请求/响应协议,每次请求都需要重新建立连接。在一些特殊的业务场景下,服务端需要主动发送数据到客户端,例如行情推送、监控告警推送等。然而,HTTP协议不支持双向通信,因此需要将HTTP协议“升级”为WebSocket协议。WebSocket协议可以在建立连接后保持连接状态,双方可以通过一个持久的连接通道进行实时通信。WebSocket连接在建立时通过HTTP协议进行握手,之后的数据传输就可以使用WebSocket协议进行。</p><p>Nginx作为中间层的Web服务器,支持使用多种协议与上下游进行通信,包括TCP、HTTP、WebSocket等协议,如下图所示。</p><p><img src="/img/%E5%9B%BE1.png" alt="图1"></p><h2 id="1、nginx升级http为websocket的过程"><a href="#1、nginx升级http为websocket的过程" class="headerlink" title="1、nginx升级http为websocket的过程"></a>1、nginx升级http为websocket的过程</h2><p>HTTP/1.1提供了一种特殊的机制,这一机制允许将一个已建立的连接升级成新的、不相容的协议。具体过程如下:</p><p><img src="/img/%E5%9B%BE2.png" alt="图2"></p><p>1.客户端发起 WebSocket 连接请求到 Nginx,Nginx 作为反向代理服务器,将请求转发给上游 WebSocket 服务器。客户端发送的请求类似于下图所示:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs css">GET ws://<span class="hljs-number">10.40</span>.xx.xx:<span class="hljs-number">58088</span>/wss/socket.io HTTP/<span class="hljs-number">1.1</span><br>Host: <span class="hljs-number">10.40</span>.xx.xx:<span class="hljs-number">58088</span><br>Connection: Upgrade<br>Upgrade: websocket<br>Sec-WebSocket-Version: <span class="hljs-number">13</span><br>Sec-WebSocket-Key: <span class="hljs-number">3</span>baOagQNXoc1Cd1dJ4pBiA==<br>Sec-WebSocket-Extensions:permessage-deflate;client_max_window_bits<br></code></pre></td></tr></table></figure><p>2.上游 WebSocket 服务器响应连接请求并完成握手协议,如果允许升级,将响应状态码101返回给 Nginx。服务端的响应类似于下图所示:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs css">HTTP/<span class="hljs-number">1.1</span> <span class="hljs-number">101</span> Switching Protocols<br>Server:xxx<br>Date:Wed,<span class="hljs-number">22</span> Feb <span class="hljs-number">2023</span> <span class="hljs-number">06</span>:<span class="hljs-number">23</span>:<span class="hljs-number">49</span> GMT<br>connection:upgrade<br>upgrade:websocket<br>Sec-WebSocket-accept:VVN2Pd9jkG7b8ur3otAk+Ah3bsg=<br>Sec-WebSocket-Extensions:permessage-deflate<br></code></pre></td></tr></table></figure><p>3.Nginx 收到上游 WebSocket 服务器的响应结果后,将其转发给客户端,建立起客户端与上游 WebSocket 服务器的连接。</p><p>4.客户端和上游 WebSocket 服务器之间开始进行实时数据传输。</p><p>5.当客户端或上游 WebSocket 服务器需要发送数据时,数据将通过 WebSocket 协议封装成帧(frame)并发送到对方。</p><p>6.数据通过 Nginx 进行转发时,Nginx 会根据实际情况选择合适的负载均衡算法,将数据传输到适当的上游 WebSocket 服务器。</p><p>7.上游 WebSocket 服务器收到数据后,解析数据帧并处理数据,然后将响应结果封装成帧并发送回客户端。</p><p>8.客户端收到上游 WebSocket 服务器的响应结果后,解析数据帧并处理数据,完成一次数据交互。</p><h2 id="2、nginx核心代码实现"><a href="#2、nginx核心代码实现" class="headerlink" title="2、nginx核心代码实现"></a>2、nginx核心代码实现</h2><h3 id="2-1、连接升级"><a href="#2-1、连接升级" class="headerlink" title="2.1、连接升级"></a>2.1、连接升级</h3><p>当服务端同意升级为 WebSocket 时,会将响应状态码设置为 “101 Switching Protocols”,表示服务器正在切换协议。如下代码所示,在处理 HTTP 协议时,首先会检查连接是否已升级到另一个协议。其中,NGX_HTTP_SWITCHING_PROTOCOLS 是一个宏,值为 101。然后,会检查客户端的请求是否携带 Upgrade 头部。如果请求携带了该头部,就会将 u->upgrade 的值设为 1,表示该连接是一个升级连接。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">//检查响应的状态码</span><br><span class="hljs-keyword">if</span> (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS) {<br> u->keepalive = <span class="hljs-number">0</span>;<br> <span class="hljs-comment">//客户端请求头是否含有upgrade头部</span><br> <span class="hljs-keyword">if</span> (r->headers_in.upgrade) {<br> u->upgrade = <span class="hljs-number">1</span>;<br> }<br> }<br></code></pre></td></tr></table></figure><h3 id="2-2、上下游数据处理"><a href="#2-2、上下游数据处理" class="headerlink" title="2.2、上下游数据处理"></a>2.2、上下游数据处理</h3><p>nginx基于事件驱动模型,当有事件触发时,就会调用对应的回调函数。</p><p>下游:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">if</span> (ev->write) {<br> r->write_event_handler(r);<br>} <span class="hljs-keyword">else</span> {<br> r->read_event_handler(r);<br>}<br></code></pre></td></tr></table></figure><p>上游:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">if</span> (ev->write) {<br> u->write_event_handler(r, u);<br> } <span class="hljs-keyword">else</span> {<br> u->read_event_handler(r, u);<br> }<br></code></pre></td></tr></table></figure><p>每个连接都根据类型(http、websocket等)的不同,设置不同的回调,下面的代码展示了当连接的u->upgrade = 1,即为websocket协议时,设置的上下游读写回调函数。当此连接再有读写事件时,就会回调下面设置的函数</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">//接收来自upstream的数据 </span><br>u->read_event_handler = ngx_http_upstream_upgraded_read_upstream;<br><span class="hljs-comment">//向upstream发送数据</span><br>u->write_event_handler = ngx_http_upstream_upgraded_write_upstream;<br><span class="hljs-comment">//接收来自客户端的数据</span><br>r->read_event_handler = ngx_http_upstream_upgraded_read_downstream;<br><span class="hljs-comment">//向客户端发送数据</span><br>r->write_event_handler = ngx_http_upstream_upgraded_write_downstream;<br></code></pre></td></tr></table></figure><p>实际上,这四个回调函数都调用了同一个处理函数。但是,它们分别传入了不同的参数,因此函数的处理方式也不同,对应四种不同的类型。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_http_upstream_upgraded_read_upstream</span><span class="hljs-params">(<span class="hljs-type">ngx_http_request_t</span> *r,</span><br><span class="hljs-params"> <span class="hljs-type">ngx_http_upstream_t</span> *u)</span><br>{<br> ngx_http_upstream_process_upgraded(r, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>);<br>}<br><span class="hljs-type">static</span> <span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_http_upstream_upgraded_write_upstream</span><span class="hljs-params">(<span class="hljs-type">ngx_http_request_t</span> *r,</span><br><span class="hljs-params"> <span class="hljs-type">ngx_http_upstream_t</span> *u)</span><br>{<br> ngx_http_upstream_process_upgraded(r, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>);<br>}<br><span class="hljs-type">static</span> <span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_http_upstream_upgraded_read_downstream</span><span class="hljs-params">(<span class="hljs-type">ngx_http_request_t</span> *r)</span><br>{<br> ngx_http_upstream_process_upgraded(r, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);<br>}<br><span class="hljs-type">static</span> <span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_http_upstream_upgraded_write_downstream</span><span class="hljs-params">(<span class="hljs-type">ngx_http_request_t</span> *r)</span><br>{<br> ngx_http_upstream_process_upgraded(r, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>);<br>}<br></code></pre></td></tr></table></figure><p>此处的设计非常巧妙(部分代码),使用了不同参数复用了同一个函数。下面的核心代码展示了 Nginx 如何处理事件。</p><p>当数据来自于服务端时,from_upstream 为真,do_write 表示需要发送数据。根据下面的代码,可以看出 src 表示服务端的连接,dst 表示客户端的连接。当 do_write 为 1 且 dst 准备好写入操作(并且需要发送的数据长度不为 0)时,dst 就会发送数据;当 src 准备好读取操作时,src 就会读取数据。</p><p>比较有意思的是,当数据来自于客户端时,只需要将 dst 和 src 交换即可。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_http_upstream_process_upgraded</span><span class="hljs-params">(<span class="hljs-type">ngx_http_request_t</span> *r,</span><br><span class="hljs-params"> <span class="hljs-type">ngx_uint_t</span> from_upstream, <span class="hljs-type">ngx_uint_t</span> do_write)</span><br>{<br> <span class="hljs-comment">//客户端连接</span><br> downstream = r->connection;<br> <span class="hljs-comment">//服务端连接</span><br> upstream = r->upstream->peer.connection;<br> <br> <span class="hljs-comment">// 如果数据来自于后端服务器</span><br> <span class="hljs-keyword">if</span> (from_upstream) {<br> src = upstream;<br> dst = downstream;<br> b = &u->buffer;<br> <span class="hljs-comment">// 如果数据来自于客户端</span><br> } <span class="hljs-keyword">else</span> {<br> src = downstream;<br> dst = upstream;<br> b = &u->from_client;<br> }<br> <span class="hljs-keyword">for</span> ( ;; ) {<br> <span class="hljs-comment">// 判断是否需要发送数据</span><br> <span class="hljs-keyword">if</span> (do_write) {<br> <span class="hljs-comment">// 获取当前要发送的数据长度</span><br> size = b->last - b->pos;<br> <span class="hljs-comment">// 如果要发送的数据长度不为 0,且连接已经准备好进行写操作</span><br> <span class="hljs-keyword">if</span> (size && dst->write->ready) {<br> <span class="hljs-comment">// 发送数据</span><br> n = dst->send(dst, b->pos, size);<br> }<br> }<br> size = b->end - b->last;<br> <span class="hljs-comment">// 如果要接收的数据长度不为 0,且连接已经准备好读操作</span><br> <span class="hljs-keyword">if</span> (size && src->read->ready) {<br> <span class="hljs-comment">// 接收数据</span><br> n = src->recv(src, b->last, size);<br> }<br> <span class="hljs-keyword">break</span>;<br> }<br>}<br></code></pre></td></tr></table></figure><h2 id="3、使用实例"><a href="#3、使用实例" class="headerlink" title="3、使用实例"></a>3、使用实例</h2><p>本节将通过 Nginx 和 WebSocket 客户端/服务端的实例,展示如何在实际业务中使用 WebSocket 进行消息推送。</p><p>首先,需要对 Nginx进行路由配置,如下图所示。即,所有以 uri 前缀为 wss 的 HTTP 客户端请求都会被升级为 WebSocket。在代理过程中,HSIAR 还需要将 Connection 和 Upgrade 头部携带给后端服务,告知后端需要将 Nginx 与后端的连接升级为 WebSocket。</p><p><img src="/img/%E5%9B%BE3.png" alt="图3"></p><p>2、建立连接的过程如下图所示,可以看到与2.1节所述过程一致</p><p><img src="/img/%E5%9B%BE4.png" alt="图4"></p><p>3、连接建立成功后,点击订阅,即接收消息的推送</p><p><img src="/img/%E5%9B%BE5.png" alt="图5"></p><p>4、后端推送消息</p><p>可以看到消息由后端成功推送到了客户端</p><p><img src="/img/%E5%9B%BE6.png" alt="图6"></p><h2 id="4、总结"><a href="#4、总结" class="headerlink" title="4、总结"></a>4、总结</h2><p>在实际的生产中,消息推送是一种常用的技术。然而,如果在 WebSocket 客户端和服务端之间的链路中加入代理,尤其是多级代理后,情况就会变得更加复杂。为了确保链路上的每一条连接都是 WebSocket 长连接,需要避免中间出现 HTTP 短链接。否则,推送就可能因连接提前断开而失败。了解该技术的原理和细节,可以帮助快速排查问题并进行修复。同时,研究 Nginx 对 WebSocket 的支持技术实现,不仅能够提高对该技术的理解,也能够为今后开发相关系统提供有益的借鉴。</p>]]></content>
<tags>
<tag>nginx</tag>
<tag>websocket</tag>
</tags>
</entry>
<entry>
<title>协商缓存在nginx的应用与实践</title>
<link href="/2024/04/12/%E5%8D%8F%E5%95%86%E7%BC%93%E5%AD%98%E5%9C%A8nginx%E7%9A%84%E5%BA%94%E7%94%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5/"/>
<url>/2024/04/12/%E5%8D%8F%E5%95%86%E7%BC%93%E5%AD%98%E5%9C%A8nginx%E7%9A%84%E5%BA%94%E7%94%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5/</url>
<content type="html"><![CDATA[<h2 id="1、前言"><a href="#1、前言" class="headerlink" title="1、前言"></a>1、前言</h2><p>缓存是一个高效减轻网络与服务器压力的机制,具有减少冗余数据传输、缓解网络瓶颈以及降低时延等优点。通常客户端在请求数据时,会发送请求到原始服务器获取,重复的数据可能会在网络中多次传输,但是如果有缓存,客户端就可以直接从缓存中获取数据,减少重复的流量。例如在浏览器首次请求某些静态资源时,状态码会是200 ok,但是刷新页面,状态码就会变为200 ok ( from memory cache),这是因为浏览器对这些资源进行了缓存,客户端的数据并不是发送请求到原始服务器获取的,而是从缓存中获取的。</p><p><img src="/img/%E7%BC%93%E5%AD%98%E6%B5%81%E7%A8%8B%E5%9B%BE.png" alt="缓存流程图"></p><p>但是问题也是显而易见的,如果原始服务器的数据发生了改变,而缓存并没有及时更新数据,在客户端请求时返回了过期的数据,这就会导致了数据的不准确。已缓存的数据应当与原始服务器的数据保持一致,更准确的来说,是缓存返回的数据应当与原始服务器的数据保持一致。那么如何在缓存的基础上,避免这个问题呢?事实上,HTTP协议是提供了多种机制来保证数据一致性的。</p><h2 id="2、“使用期”与“新鲜度“"><a href="#2、“使用期”与“新鲜度“" class="headerlink" title="2、“使用期”与“新鲜度“"></a>2、“使用期”与“新鲜度“</h2><p>使用期是指数据在服务器响应返回后的总时间,可以简单理解为数据在缓存使用的时间,从服务器将数据发出去开始计时;新鲜度是指数据在服务器响应发出去后,缓存可以使用的时间。如果使用期小于新鲜度,说明数据是“新鲜的”,缓存可以继续使用。反之,缓存需要判断数据是否发生了更新,是否需要重新拉取数据,如何更新这取决于服务端采用的HTTP缓存策略。</p><p><strong>使用期:</strong></p><p>服务器用HTTP协议的响应头部date表示发送数据时的时间,如果客户端与服务端使用同样的、完全精确的时钟,已缓存数据的使用期(data_age)就可以是当前时间(current_time)减去服务器发送数据时(Date_header_value)的时间。</p><p>data_age = current_time – date_header_value</p><p>但是并不是所有的计算机都实现了时钟同步,当服务器和客户端的时钟不同步时,使用期可能是很大或者甚至是负的,如果是负的,就需要将其设置为零。</p><p>data_age = max(0,current_time – date_header_value)</p><p>date_header_value的值代表着原始服务器发出数据的时间,所以在经过代理时,一定不能进行修改。</p><p><strong>新鲜度:</strong></p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs css">Expires : Fri, <span class="hljs-number">09</span> Sep <span class="hljs-number">2022</span>, <span class="hljs-number">05</span>:<span class="hljs-number">27</span>:<span class="hljs-number">57</span> GMT<br><br>Cache-Control : max-age=<span class="hljs-number">3600</span><br></code></pre></td></tr></table></figure><p>服务器用HTTP/1.0+的Expires或HTTP/1.1的Cache-Control:max-age响应头部指定数据的过期时间。Expires指定的是绝对时间,即数据到这个时间就过期了。而Cache-Control:max-age指定的是相对时间,表示缓存收到数据后可以在缓存存活的时间。由于Expires依靠于时钟的准确性,因此目前更多的使用后者。</p><p>通过比对使用期与新鲜度,缓存可以判断当前存储的数据是否足够新鲜,如果足够新鲜,则直接返回缓存中的数据,不然就只能重新从原始服务器拉取数据。但是数据在缓存中已经过期,而原始服务器中并未发生更新,缓存依旧需要发送请求获取数据,这会消耗大量不必要的网络资源。对于网络传输而言,应当遵守以最小数据量传输而保证最大信息量传输的原则,因此为了减少冗余数据的传输,HTTP协议提供了协商缓存机制,用以减少数据的传输。</p><p><img src="/img/%E4%BD%BF%E7%94%A8%E6%9C%9F%E4%B8%8E%E6%96%B0%E9%B2%9C%E5%BA%A6.png" alt="使用期与新鲜度"></p><h2 id="3、协商缓存:no-store、no-cache与must-revalidate"><a href="#3、协商缓存:no-store、no-cache与must-revalidate" class="headerlink" title="3、协商缓存:no-store、no-cache与must-revalidate"></a>3、协商缓存:no-store、no-cache与must-revalidate</h2><figure class="highlight https"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs https">HTTP/1.0:<br><br> Pragma: no-cache<br><br>HTTP/1.1:<br><br> Cache-Control: no-store<br><br> Cache-Control: no-cache<br><br> Cache-Control: must-revalidate<br></code></pre></td></tr></table></figure><p>服务器可以通过no-store来禁止缓存对数据进行存储,因此每次客户端请求数据时,缓存都需要发送请求到原始服务器获取,这样就可以保证客户端获取数据的新鲜度。</p><p>no-cache与no-store不同,no-cache允许缓存对数据进行存储,缓存需要在原始服务器验证新鲜度之后,才能将数据返回给客户端。通常,如果数据发生了更新,原始服务器会返回更新后的数据;反之,会返回304,表示缓存的数据并没有发生改变,可以把缓存的数据返回给客户端。</p><p>而must-revalidate与no-cache类似,同样允许缓存对数据进行存储。如果缓存的数据过期,则must-revalidate与no-cache的行为一致;但是如果数据未过期,则可以直接返回给客户端数据而无需验证。因此must-revalidate通常需要与Expires、max-age进行配合使用。例如:</p><p><img src="/img/cache-%E5%9B%BE3.png" alt="Cache-Control的使用"></p><p>现在让我们总结一下这3种缓存机制的特点:</p><p>no-store:缓存不可以存储数据,每次请求都需要到服务器获取数据,因此可以保证数据的新鲜度,但是大量冗余数据的传输,会增大网络与服务器的压力,降低系统的整体性能。</p><p>no-cache:缓存可以存储数据,每次请求都需要到服务器进行新鲜度的验证,因此可以保证数据的新鲜度。相比于no-store,减少了大量数据的传输。</p><p>must-revalidate:缓存可以存储数据,如果数据过期,则需要到服务器进行新鲜度的验证,反之,则可以直接返回给客户端数据。相比于no-cache,减少一定数量的新鲜度验证请求,进一步减少网络与服务器的压力,但是不能保证数据的新鲜度,有一定时间的误差,这取决于新鲜度的设置。</p><p>3种机制各有优劣,应该根据具体的业务需求选择合适的缓存机制,但整体来看,no-cache适合绝大部分的场景。 </p><h3 id="3-1、no-cache验证新鲜度:if-modified-since与if-none-match"><a href="#3-1、no-cache验证新鲜度:if-modified-since与if-none-match" class="headerlink" title="3.1、no-cache验证新鲜度:if-modified-since与if-none-match"></a>3.1、no-cache验证新鲜度:if-modified-since与if-none-match</h3><p>当原始服务器采用no-cache缓存模式时,缓存请求数据,服务器的响应会返回响应头Last-Modified与Etag。Last-Modified表示原始服务器修改该数据的最后时间,Etag是一个字符串,不同的系统生成方式也是不同的。缓存在验证新鲜度时,会将这2个值通过2个请求头If-Modified-Since与If-None-Match传送到原始服务器,而原始服务器通过比对这2个值,就可以判断缓存的数据与本地数据是否一致,也就可以决定是否需要返回数据。</p><p>响应:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs css">Last-Modified : Tue,<span class="hljs-number">06</span> Sep <span class="hljs-number">2022</span> <span class="hljs-number">03</span>:<span class="hljs-number">09</span>:<span class="hljs-number">17</span> GMT<br><br>ETag : “<span class="hljs-number">6316</span>b9dd-<span class="hljs-number">2</span>b1ce”<br></code></pre></td></tr></table></figure><p>请求:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs css">If-Modified-Since : Tue,<span class="hljs-number">06</span> Sep <span class="hljs-number">2022</span> <span class="hljs-number">03</span>:<span class="hljs-number">09</span>:<span class="hljs-number">17</span> GMT<br><br>If-None-Match : “<span class="hljs-number">6316</span>b9dd-<span class="hljs-number">2</span>b1ce”<br></code></pre></td></tr></table></figure><h2 id="4、协商缓存在nginx的应用"><a href="#4、协商缓存在nginx的应用" class="headerlink" title="4、协商缓存在nginx的应用"></a>4、协商缓存在nginx的应用</h2><h3 id="4-1、应用no-cache对前端的优化"><a href="#4-1、应用no-cache对前端的优化" class="headerlink" title="4.1、应用no-cache对前端的优化"></a>4.1、应用no-cache对前端的优化</h3><p>nginx提供流量分发、协议转换、静态资源代理等功能。本节以HUI前端为例,围绕静态资源代理这一功能,分析nginx何应用no-cache优化前端。</p><p>可以对协商缓存进行设置,最后落地到配置文件nginx.conf,具体配置如下图所示,可以设置单个文件采用协商缓存模式如: sysconfig.js;也可以根据文件类型后缀设置协商缓存模式如:html或js。</p><p><img src="/img/cache-4.png" alt="cache-4"></p><p>首先在浏览器访问前端,可以看到首次访问时,静态资源的状态码是200 ok,这代表数据是从服务器获取到的。</p><p><img src="/img/cache-5.png" alt="cache-5"></p><p>接着刷新界面,可以看到状态码变为304 Not Modified,nginx的日志信息也可以看到客户端是发起了一次请求到nginx获取数据,判断到数据并未发生更新返回304。</p><p><img src="/img/cache-6.png" alt="cache-6"></p><p><img src="/img/cache-7.png" alt="cache-7"></p><p>此时,如果对sysconfig.js进行修改,再次刷新界面,如下图所示,可以看到状态码变为200 ok,修改的内容及时返回到了客户端。</p><p>修改前:</p><p><img src="/img/cache-8.png" alt="cache-8"></p><p>修改:</p><p><img src="/img/cache-9.png" alt="cache-9"></p><p>接着刷新界面,可以看到状态码变为200 ok,而且数据已经更新:</p><p><img src="/img/cache-10.png" alt="cache-10"></p><p><img src="/img/cache-11.png" alt="cache-11"></p><p> 这样既可以保证数据的及时更新,又可以减少大量数据的传输,唯一的网络开销是进行新鲜度的再次验证。</p><h3 id="4-2、nginx源码分析If-Modified-Since与If-None-Match"><a href="#4-2、nginx源码分析If-Modified-Since与If-None-Match" class="headerlink" title="4.2、nginx源码分析If-Modified-Since与If-None-Match"></a>4.2、nginx源码分析If-Modified-Since与If-None-Match</h3><p><strong>etag的生成</strong></p><p>nginx的etag的生成方式比较简单,由last-modified_time与content_length_n转换为十六进制组合而成。其中last-modified_time是通过系统调用,获取的文件最后修改时间,对应操作系统文件结构stat中st_mtime; content-length_n是文件的大小,对应操作系统文件结构stat中st_size;</p><p><img src="/img/cache-12.png" alt="cache-12"></p><p>缓存请求数据时,在响应头返回给缓存。</p><p><img src="/img/cache-13.png" alt="cache-13"></p><p><strong>If-Modified-Since</strong>与<strong>If-None-Match</strong>的校验</p><p>缓存向nginx验证数据新鲜度时,需要携带If-Modified-Since与If-None-Match请求头。</p><p><img src="/img/cache-14.png" alt="cache-14"></p><p>nginx在判断缓存数据的新鲜度时,会先后对If-Modified-Since和If-None-Match与当前数据的last-modified和etag进行比对,只要2者有1个发生了改变,则判断本地数据发生更新,缓存中的数据已过期,就会直接返回更新后的数据,如果都没有变,则会返回304。源码与流程图如下。</p><p><img src="/img/cache-15.png" alt="cache-15"></p><p><img src="/img/cache-16.png" alt="cache-16"></p><h2 id="5、实践出真知"><a href="#5、实践出真知" class="headerlink" title="5、实践出真知"></a>5、实践出真知</h2><h3 id="5-1、抓包分析-协商缓存验证新鲜度"><a href="#5-1、抓包分析-协商缓存验证新鲜度" class="headerlink" title="5.1、抓包分析-协商缓存验证新鲜度"></a>5.1、抓包分析-协商缓存验证新鲜度</h3><p>1、首先用nginx代理一个js文件,浏览器第一次请求资源时,状态码为200 ok,刷线界面,状态码变为304,如下图</p><p><img src="/img/cache-17.png" alt="cache-17"></p><p>为了兼容http1.0,pragma也设置了no-cache</p><p><img src="/img/cache-18.png" alt="cache-18"></p><p><img src="/img/cache-19.png" alt="cache-19"></p><p>2、抓包查看第一次请求的网络包,可以看到服务端返回了静态资源的数据</p><p><img src="/img/cache-20.png" alt="cache-20"></p><p>3、查看第二次请求,可以看到服务端没有返回任何静态资源,只有响应头这些数据</p><p><img src="/img/cache-21.png" alt="cache-21"></p><p>4、符合协商缓存的现象</p><p>当请求的If-Modified-Since、If-None-Match与服务端不一致时,服务端会返回静态资源</p><p><img src="/img/cache-22.png" alt="cache-22"></p><p>但是当请求的If-Modified-Since、If-None-Match与服务端一致时,服务端验证新鲜度足够,就只会返回304</p><p><img src="/img/cache-23.png" alt="cache-23"></p><h3 id="5-2、一个GET请求被缓存导致的登录异常"><a href="#5-2、一个GET请求被缓存导致的登录异常" class="headerlink" title="5.2、一个GET请求被缓存导致的登录异常"></a>5.2、一个GET请求被缓存导致的登录异常</h3><p>首先来看下定义:</p><p>在HTTP规范中,GET请求通常用于从服务器检索数据,而不改变服务器的状态。这种操作被认为是安全的和幂等的,因此响应是可以缓存的。</p><p>HTTP规范规定,POST请求是用来提交数据的,可能会导致服务器状态的改变,因此其响应不应被缓存。</p><p>我们的网关层实现了cas单点登录,其中登录过程会调用一个免密接口,主要用于从权限系统获取权限数据以及一些会话数据,这个请求是get请求。</p><p>问题:当登录成功以后,点击浏览器的回退,这时由于浏览器的TGC没有过期,照理应该重新登录进系统,但是登录后发现,页面空白,请求出现401权限丢失的情况,我发现浏览器并没有真实发起免密接口的调用,而是获取了缓存的数据,这时问题就很明确了。</p><p><img src="/img/cache-24.png" alt="cache-24"></p><p>1、接口的设计不好,这个接口会改变服务器的状态,应该设计为post,但是被错误的设计为get</p><p>2、如果是get也可以兼容,这个接口在返回响应时,增加no-store的响应头,禁止前端缓存,需要注意的是,这个时候用no-cache是不行的。</p><p>照理,接口改为post是最正确的做法,但是现实是要考虑兼容的问题,我们网关层也是需要做修改的。</p><h2 id="6、总结"><a href="#6、总结" class="headerlink" title="6、总结"></a>6、总结</h2><p>协商缓存不只是一种简单的缓存机制,更是一种很好的理念。对于客户端与服务端数据同步、性能优化都是很好的借鉴,尤其是服务端不能主动向客户端发送请求的场景。例如当有服务以域名的形式注册到nginx时,nginx需要向DNS查询真实ip,为了避免每次请求都会向DNS查询,会对查询到的结果进行缓存,并启动定时器查询真实ip是否发生改变。而定时器的时间就类似于协商缓存的新鲜度,在实际的生产中没有完美的方案,因此需要根据具体的需求偏重来调整可靠性与性能。</p><p>本文从HTTP缓存原理出发,介绍了缓存对系统性能优化的意义,并讲解了HTTP缓存发展过程中存在冗余数据多次传输的问题,以及为了解决这个问题而出现的协商缓存机制。通过对协商缓存的原理与nginx实现协商缓存的源码分析,希望大家可以对HTTP缓存有一定的理解。 </p>]]></content>
<tags>
<tag>协商缓存</tag>
<tag>no cache nginx</tag>
</tags>
</entry>
<entry>
<title>ssl双向验证— ssl_verify_depth的作用</title>
<link href="/2024/04/12/ssl%E5%8F%8C%E5%90%91%E9%AA%8C%E8%AF%81%E2%80%94%20ssl_verify_depth%E7%9A%84%E4%BD%9C%E7%94%A8/"/>
<url>/2024/04/12/ssl%E5%8F%8C%E5%90%91%E9%AA%8C%E8%AF%81%E2%80%94%20ssl_verify_depth%E7%9A%84%E4%BD%9C%E7%94%A8/</url>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p><strong>关键词</strong>:根证书、中间证书、验证深度、ssl_verify_depth</p><h2 id="根证书与中间证书"><a href="#根证书与中间证书" class="headerlink" title="根证书与中间证书"></a>根证书与中间证书</h2><p>在进行ssl验证前,服务器一般会向CA申请公钥证书,即将自己的公开密钥交给CA,CA用自己的私钥向服务器的公钥数字签名并返回公钥证书,在数字签名的过程中,CA一般会用根目录颁发证书,这种证书叫做根证书。</p><p>问题是,万一根目录颁发错了证书,或者需要撤销根,这时所有根目录颁发的证书都将失效,这样代价是巨大的,因此出现了<strong>中间根</strong>,顾名思义,CA用私钥对中间根进行签名,使它可信,因此由中间根颁发的证书也是可信的,即中间证书。当发生撤销时,只需要撤销中间根颁发的证书就可以。</p><p>这里需要解释一下根证书,个人理解为客户端在验证服务器的公钥证书时,需要拿CA的公钥来解密服务器公钥证书的签名,CA的<strong>根公钥</strong>需要提前拿到手,一般内置到浏览器中,存放的地方视为根目录,存放中间证书即为中间目录。有中间证书的情况下,应该是先从中间目录取到对应<strong>中间公钥</strong>解密,然后循环此过程,直到从根目录拿到公钥验证成功,这时可以算是验证通过。</p><p>同时,在实际生产中,我们拥有私有协议,会存在私有协议的客户端,这些客户端也支持<strong>私有协议</strong>+<strong>ssl</strong>,因此这些客户端也需要内置根证书。</p><h2 id="验证深度"><a href="#验证深度" class="headerlink" title="验证深度"></a>验证深度</h2><p>在CA的证书体系中,证书从根目录出发,像一条链一样,有很多的中间根,也叫做证书链,我觉得更像一棵二叉树。</p><p>在ssl验证的过程中,直接尝试中间证书进行客户端认证是无法通过的,需要一层一层回溯验证,直到找到根。</p><p>这个验证深度就相当于当前中间证书在整棵树中的深度。</p><h2 id="ssl-verify-depth"><a href="#ssl-verify-depth" class="headerlink" title="ssl_verify_depth"></a>ssl_verify_depth</h2><p>上面已经提到了,验证需要层层回溯,向上可以回溯多少次由ssl_verify_depth决定,当<code>ssl_verify_depth = 1</code>时,回溯层数为0,即任何中间证书都不会通过验证,除非是根证书。简而言之,中间证书的深度要小于ssl_verify_depth的值,才会验证通过。</p><p>在nginx中<code>ssl_verify_depth</code>的值默认是为1的。所以如果使用了中间证书,就需要适当调整这个值。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs c">句法: ssl_verify_depth number;<br>默认: ssl_verify_depth <span class="hljs-number">1</span>;<br>语境: http, server<br></code></pre></td></tr></table></figure>]]></content>
<tags>
<tag>ssl</tag>
<tag>证书</tag>
</tags>
</entry>
<entry>
<title>SOCKET.IO最佳实践-代理篇</title>
<link href="/2024/04/12/SOCKET.IO%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5-%E4%BB%A3%E7%90%86%E7%AF%87/"/>
<url>/2024/04/12/SOCKET.IO%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5-%E4%BB%A3%E7%90%86%E7%AF%87/</url>
<content type="html"><![CDATA[<h2 id="前言:"><a href="#前言:" class="headerlink" title="前言:"></a>前言:</h2><p>在传统的轮询中,客户端定期向服务器发送请求,询问是否有新的数据可用。这会导致很多不必要的空请求,尤其是在没有新数据可用的时候。而且如果使用的是HTTP/1.0版本,每个请求/响应都需要打开一个新连接,考虑到连接的建立、关闭、TCP慢启动机制等因素,这是一个很大的开销。因此HTTP/1.1引入了2个头部:Connection头部和Upgrade头部,用于协议升级。</p><p>其中Connection: keep-alive可以将HTTP短链接升级为长连接,这意味着在一个 TCP 连接上可以传输多个 HTTP 请求和响应,这样就减少大量请求建立、关闭等因素的开销,并且依靠这个机制,可以实现一种长轮询的模式,进一步减少空请求的损耗。需要注意的是,HTTP/1.0也可以使用Connection: keep-alive,但是服务端并不一定支持,因此尽可能使用HTTP/1.1版本,同时本文后续出现的HTTP,默认指的都是HTTP/1.1。</p><p>同样,可以使用Connection: Upgrade与Upgrade: websocket将HTTP连接升级为websocket,具体可以参考本人另外一篇文章《浅析nginx实现websocket原理》。</p><h2 id="1、HTTP-长轮询:"><a href="#1、HTTP-长轮询:" class="headerlink" title="1、HTTP 长轮询:"></a>1、HTTP 长轮询:</h2><p>HTTP长轮询通过使用Connection: keep-alive实现服务端消息的“推送”。具体过程如下:</p><p>1.客户端发送一个HTTP请求到服务器,但服务器不立即响应。客户端发送的请求类似于下图所示:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs css">GET /socket<span class="hljs-selector-class">.io</span>/?EIO=<span class="hljs-number">4</span>&transport=polling&t=OtBbTrW HTTP/<span class="hljs-number">1.1</span><br>Cache-Control: no-cache<br>Connection: keep-alive<br>Host: <span class="hljs-number">127.0</span>.<span class="hljs-number">0.1</span>:<span class="hljs-number">3001</span><br>Pragma: no-cache<br></code></pre></td></tr></table></figure><p>2.服务器保持请求打开,等待有新的数据或事件发生。</p><p>3.一旦有新的数据或事件发生,服务器立即响应请求,将数据传输给客户端。服务端的响应类似于下图所示:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs css">HTTP/<span class="hljs-number">1.1</span> <span class="hljs-number">200</span> OK<br><span class="hljs-attribute">Content</span>-Type: text/plain; charset=UTF-<span class="hljs-number">8</span><br><span class="hljs-attribute">Content</span>-Length: <span class="hljs-number">118</span><br>cache-control: no-store<br>Date: Wed, <span class="hljs-number">21</span> Feb <span class="hljs-number">2024</span> <span class="hljs-number">12</span>:<span class="hljs-number">13</span>:<span class="hljs-number">32</span> GMT<br>Connection: keep-alive<br>Keep-Alive: timeout=<span class="hljs-number">5</span><br></code></pre></td></tr></table></figure><p>Keep-Alive: timeout=5 表示服务器愿意在响应后保持连接打开,等待可能的进一步请求,而这个连接将在5秒钟后自动关闭,除非另外有新的请求。</p><p>4.客户端收到响应后,立即再次发起新的HTTP请求,重复上述过程。</p><p>相比于传统轮询,HTTP长轮询减少了不必要的空请求,因为服务器只在有新数据时才会响应。并且这种方式可以降低通信的延迟,因为服务器在有数据时立即将其传输给客户端,而不需要等到下一次定期轮询。</p><p>但是服务器必须维护大量的打开连接,这可能导致服务器资源的浪费。而且在某些情况下,中间代理(如代理服务器或防火墙)可能会中断长轮询连接,导致不稳定的通信。</p><p>所以对于服务端推送消息的场景,websocket是一种更好的方式,HTTP长轮询只是提供了一种在不支持WebSocket的环境中实现实时通信的方法。</p><h2 id="2、Socket-io"><a href="#2、Socket-io" class="headerlink" title="2、Socket.io"></a>2、Socket.io</h2><p>Socket.IO 是一个库,可以在客户端和服务器之间实现低延迟, 双向和基于事件的通信。Socket.IO在普通的websocket上提供一些功能,如自动重新连接、广播、HTTP长轮询回退(无法与服务端建立websocket连接,将回退为HTTP长轮询)等功能。因此一个socket.io客户端和服务端的交互过程中,可能会同时存在HTTP长轮询与websocket协议的请求。在存在代理的链路中,不当的配置会导致通信失败,本文将着重分析在多级代理中,如何正确配置使得socket.io客户端与服务端正常通信。</p><h3 id="2-1、会话id"><a href="#2-1、会话id" class="headerlink" title="2.1、会话id"></a>2.1、会话id</h3><p>在 Socket.IO 中,每个客户端连接都会被分配一个唯一的标识符,通常被称为会话id,会话id在服务端会关联客户端的连接。所有后续HTTP请求的参数中必需携带这个值,这个标识符可以用于在服务器端跟踪和识别特定的客户端连接。</p><p>通过这种标识符,服务器可以维护一个连接池,用于管理和处理来自不同客户端的实时通信。这对于实现诸如广播消息、单播消息、断线重连等功能都非常有用。</p><p>在 Socket.IO 中,连接建立时会触发一个事件(通常是 <strong>connection</strong> 事件),服务器会分配一个唯一的 sid给客户端连接,一个HTTP长轮询请求如下图所示。</p><p><img src="/img/socket-%E5%9B%BE1.png" alt="socket-图1"></p><h3 id="2-2、socket-io集群"><a href="#2-2、socket-io集群" class="headerlink" title="2.2、socket.io集群"></a>2.2、socket.io集群</h3><p>Socket.IO客户端和服务端是靠会话id一一对应的,所以客户端请求到了错误的Socket.IO服务端时,就会报错,因为服务端识别不了。</p><p>因此,当socket.io为集群时,nginx做代理,如果负载策略是轮询,那么客户端和服务端会有概率不匹配。下图的场景,client-A的请求必须路由到第二个socket.io节点,因为nginx是轮询,因此每3笔会有一笔失败,在浏览器的现象就是,刷新2次界面后,系统恢复正常。</p><p>因此,需要在nginx开启会话保持,即ip_hash,这样一个客户端的ip会固定路由到一个Socket.IO服务端,这样就不会出现不匹配的问题。</p><p><img src="/img/socket-%E5%9B%BE2.png" alt="socket-图2"></p><h2 id="3、websocket"><a href="#3、websocket" class="headerlink" title="3、websocket"></a>3、websocket</h2><p>websocket很明显是优于HTTP长轮询的,只要维持一条长连接,就可以实现全双工通信,避免了频繁的建立连接。通常Socket.IO首先会发起HTTP长轮询请求,服务端会在响应中返回upgrades 数组,表示服务器支持更好的传输协议,如下所示。然后,socket.io客户端就会将协议升级为upgrades 数组中的一种。</p><figure class="highlight prolog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs prolog">{<br> <span class="hljs-string">"sid"</span>: <span class="hljs-string">"FSDjX-WRwSA4zTZMALqx"</span>,<br> <span class="hljs-string">"upgrades"</span>: [<span class="hljs-string">"websocket"</span>],<br> <span class="hljs-string">"pingInterval"</span>: <span class="hljs-number">25000</span>,<br> <span class="hljs-string">"pingTimeout"</span>: <span class="hljs-number">20000</span><br>}<br></code></pre></td></tr></table></figure><ul><li><p>sid 是会话的ID,它必须包含在sid所有后续HTTP请求的查询参数中</p></li><li><p>upgrades 数组包含服务器支持的所有“更好”传输的列表</p></li><li><p>pingInterval 和 pingTimeout 值用于心跳</p></li></ul><h3 id="3-1、socket-io升级websocket的过程"><a href="#3-1、socket-io升级websocket的过程" class="headerlink" title="3.1、socket.io升级websocket的过程"></a>3.1、socket.io升级websocket的过程</h3><p>1、 最开始客户端的请求</p><p><img src="/img/socket-%E5%9B%BE3.png" alt="socket-图3"></p><p>2、服务端响应</p><p><img src="/img/socket-%E5%9B%BE4.png" alt="socket-图4"></p><p>3、客户端发送请求建立websocket连接</p><p><img src="/img/socket-%E5%9B%BE5.png" alt="socket-图5"></p><p>其中请求头为</p><p><img src="/img/socket-%E5%9B%BE6.png" alt="socket-图6"></p><p>响应头为</p><p><img src="/img/socket-%E5%9B%BE7.png" alt="socket-图7"></p><p>4、心跳</p><p>心跳间隔为25s,与”pingInterval”: 25000是一致的</p><p><img src="/img/socket-%E5%9B%BE8.png" alt="socket-图8"></p><h3 id="3-2、此websocket非常规的websocket"><a href="#3-2、此websocket非常规的websocket" class="headerlink" title="3.2、此websocket非常规的websocket"></a>3.2、此websocket非常规的websocket</h3><p>Socket.IO 可以使用 WebSocket 协议,但它为每个数据包添加了额外的元数据。所以 WebSocket 客户端将无法成功连接到 Socket.IO 服务器,而 Socket.IO 客户端也将无法连接到普通 WebSocket 服务器。</p><p>我们团队提供的wss组件,虽然支持了socket.io与普通的websocket,但是同一时刻也只能支持其中的一种,即使用socket.io的客户端和websocket客户端连接wss,总有一个会失败。</p><h2 id="4、代理"><a href="#4、代理" class="headerlink" title="4、代理"></a>4、代理</h2><p>由于会话id的存在,每个携带唯一会话id的HTTP请求都必须路由到对应的socket.io服务端,尤其是socket.io服务端为多节点时。本节着重讲解如何正确配置代理节点,使得socket.io客户端与服务端可以使用HTTP长轮询与websocket进行正常通信。</p><h3 id="1、socket-io为单节点"><a href="#1、socket-io为单节点" class="headerlink" title="1、socket.io为单节点"></a>1、socket.io为单节点</h3><ul><li>HTTP长轮询</li></ul><p>socket.io为单节点时,客户端和服务端肯定是对应的,所以不管中间代理怎么路由,都没有问题。</p><ul><li>websocket</li></ul><p>中间节点都需要升级HTTP协议为websocket,如果是四层负载,那就不需要做任何改动,websocket只针对<strong>七层负载。</strong></p><h3 id="2、socket-io多节点"><a href="#2、socket-io多节点" class="headerlink" title="2、socket.io多节点"></a>2、socket.io多节点</h3><h4 id="1、一级代理"><a href="#1、一级代理" class="headerlink" title="1、一级代理"></a>1、一级代理</h4><p>1、代理为单节点</p><p><img src="/img/socket-%E5%9B%BE9.png" alt="socket-图9"></p><ul><li>HTTP长轮询</li></ul><p>由于client使用session id和socket.io端一一对应,因此需要保证同一client的请求一直路由到同一socket.io服务端,否则会报400的错误(其他服务端识别不了未知的sid)。所以nginx需要配置会话保持,即ip_hash,其他负载均衡器类似。</p><p><img src="/img/socket-%E5%9B%BE10.png" alt="socket-图10"></p><ul><li>websocket</li></ul><p>如果nginx配置了协议升级,client到nginx、nginx到socket.io的连接都是websocket协议的连接,即长连接,nginx保证了client与服务端一一对应</p><p><img src="/img/socket-%E5%9B%BE11.png" alt="socket-图11"></p><h4 id="2、多级代理"><a href="#2、多级代理" class="headerlink" title="2、多级代理"></a>2、多级代理</h4><h5 id="1、单-单"><a href="#1、单-单" class="headerlink" title="1、单-单"></a>1、单-单</h5><p>l HTTP长轮询</p><p>如果代理是单节点-单节点,如下图。为了保证客户端与服务端一一对应,那么需要在<strong>第二个nginx配置会话保持</strong>。</p><p><img src="/img/socket-%E5%9B%BE12.png" alt="socket-图12"></p><ul><li>Websocket</li></ul><p>毫无疑问,如果每个nginx都配置了websocket协议升级,将不会出现任何问题</p><h5 id="2、单-多"><a href="#2、单-多" class="headerlink" title="2、单-多"></a>2、单-多</h5><ul><li>HTTP长轮询</li></ul><p>如果代理节点是单节点-多节点,为了保证客户端与服务端一一对应,那么需要<strong>多级nginx都需要配置会话保持</strong>。这时我们应该发现了一个规律,<strong>只要某个节点后面的节点是集群,那么当前节点就需要配置会话保持</strong>,这其实就是HTTP长轮询在多级代理场景下的核心</p><p><img src="/img/socket-%E5%9B%BE13.png" alt="socket-图13"></p><p>深度思考二个问题:</p><p>1、上图第一个nginx真的需要配置ip_hash吗?</p><p>2、下图,哪些nginx需要配置ip_hash?</p><p>答案将会放在第5节,如果你能回答正确,那么你就真正理解了如何代理长轮询</p><p><img src="/img/socket-%E5%9B%BE14.png" alt="socket-图14"></p><ul><li>websocket</li></ul><p>毫无疑问,如果每个nginx都配置了websocket协议升级,将不会出现任何问题</p><h5 id="3、多-多"><a href="#3、多-多" class="headerlink" title="3、多-多"></a>3、多-多</h5><p>这种场景和单-多的场景没有任何区别,因为集群前面肯定有一个单节点的负载均衡器做负载,本质也是单-多。有些人可能好奇,双活难道不是每个节点都是集群吗,整条链路如果存在单节点,这个单节点挂掉之后,整条链路随之挂掉,事实上就是这样的,所以在负载的最前面,都是用DNS做分发。</p><h5 id="4、多-单"><a href="#4、多-单" class="headerlink" title="4、多-单"></a>4、多-单</h5><p>同上,本质和单-多没有区别</p><h4 id="3、HTTP长轮询与websocket同时存在"><a href="#3、HTTP长轮询与websocket同时存在" class="headerlink" title="3、HTTP长轮询与websocket同时存在"></a>3、HTTP长轮询与websocket同时存在</h4><p>Socket.io的机制会同时存在HTTP长轮询与websocket协议的请求,所以代理节点需要<strong>同时配置会话保持与协议升级的配置</strong>。</p><h2 id="5、问题解答"><a href="#5、问题解答" class="headerlink" title="5、问题解答"></a>5、问题解答</h2><p>1、首先回答第4节的2个问题</p><p>1、第一个nginx真的需要配置ip_hash吗?</p><p>ip_hash是将某一ip的客户端,固定路由到后台的某一台服务器。所以假设client-A、client-B与某个socket.io建立了会话,,那么后续请求也需要一一对应,我们可以推测nginx-M是否设置ip_hash的路由场景</p><ul><li>nginx-M设置了ip_hash</li></ul><p>因为client-A与client-B的ip不一样,nginx-M又设置了ip_hash,所以client-A的请求都会走到nginx-A(这是假设,事实上不走A,就会走B,这里假设走A),client-B的请求都会走到nginx-B。重点来了,<strong>nginx-M的ip是固定的,所以对于nginx-A和nginx-B而言,一样的ip,他们都会路由到同一个服务端(ip_hash的算法决定),假设都路由到了socket.io-A,因此socket.io-B其实一直是空闲的!</strong></p><ul><li>nginx-M没有设置ip_hash</li></ul><p>如果nginx-M没有设置ip_hash,client的请求,nginx-M会轮询分发到ngina-A和nginx-B,但是由于<strong>nginx-M的ip是固定的,所以对于nginx-A和nginx-B而言,一样的ip,他们都会路由到同一个服务端,假设都路由到了socket.io-A,因此socket.io-B其实一直是空闲的!</strong>,我们发现和上面一模一样,所以结论是nginx-M是不需要设置ip_hash的。</p><p><img src="/img/socket-%E5%9B%BE15.png" alt="socket-图15"></p><p>2、哪些nginx需要配置ip_hash?</p><p>2个问题的本质是一样的,事实上,只要nginx-M、nginx-C、nginx-D设置了ip_hash就可以保证客户端和服务端一一对应</p><p><img src="/img/socket-%E5%9B%BE16.png" alt="socket-图16"></p><p>2、结论</p><p>其实为了简单化问题,我们可以对每个nginx都设置ip_hash,但是需要注意的是,总会有一层的集群变成了”单点”,有节点总是处于空闲状态!,导致集群变成了单点</p><h2 id="6、socket-io集群同步"><a href="#6、socket-io集群同步" class="headerlink" title="6、socket.io集群同步"></a>6、socket.io集群同步</h2><p>试想,如果socket.io集群间能同步数据,那么是不是客户端可以随意对应哪个socket.io了?</p><p>答案是的</p><p>由于客户端可能连接到集群中不同的节点,为了在集群中不同的节点之间传递消息,socket.io官方以redis的发布订阅功能为基础做了消息路由分发:<strong>socket.io-redis</strong>。<strong>socket.io-redis</strong>在节点向客户端群发消息时会将该消息发布到redis的订阅队列中,让其他节点能够订阅到该消息,从而实现节点间消息推送。不过这有额外的开发工作量,目前来看,公司内部的socket.io并没有做集群的数据共享</p><h2 id="7、sticky-哈希-一致性哈希"><a href="#7、sticky-哈希-一致性哈希" class="headerlink" title="7、sticky&&哈希 && 一致性哈希"></a>7、sticky&&哈希 && 一致性哈希</h2><h3 id="1、不能使用ip-hash,要使用hash"><a href="#1、不能使用ip-hash,要使用hash" class="headerlink" title="1、不能使用ip_hash,要使用hash"></a>1、不能使用ip_hash,要使用hash</h3><p>nginx的ngx_http_upstream_ip_hash_module.c包含了具体hash负载策略的实现,在另外一篇文章会分析nginx的几大负载策略,这里简要概括一下,nginx会使用r->connection的地址,即取上一个节点的ip进行负载,也就是第5节说的问题,多级代理下,会导致集群退化成单节点。</p><p>同时nginx提供了hash,因此需要使用hash,而不是ip_hash,且要这么配置</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs c">upstream myapp {<br> hash $http_x_real_ip; # 使用 X-Real-IP 头部的值进行哈希<br> server backend1.example.com weight=<span class="hljs-number">1</span>;<br> server backend2.example.com weight=<span class="hljs-number">2</span>;<br> }<br><br><span class="hljs-comment">//或者</span><br><br>upstream myapp {<br> hash $http_x_forwarded_for; # 使用 $http_x_forwarded_for 头部的值进行哈希<br> server backend1.example.com weight=<span class="hljs-number">1</span>;<br> server backend2.example.com weight=<span class="hljs-number">2</span>;<br> }<br></code></pre></td></tr></table></figure><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs css">//ingress-nginx<br>nginx<span class="hljs-selector-class">.ingress</span><span class="hljs-selector-class">.kubernetes</span><span class="hljs-selector-class">.io</span>/upstream-hash-by: <span class="hljs-string">"$http_x_forwarded_for"</span><br></code></pre></td></tr></table></figure><p><strong>如果使用$http_x_forwarded_for,会将第一个地址进行哈希,还是所有地址呢?</strong></p><p>这里先来理解一下X-Forwarded-For,X-Forwarded-For包含了经过的所有代理服务器的IP地址。同样在多级代理下,每级代理(包括最初始的客户端)都需要传递X-Forwarded-For头部,并添加自身的ip .X-Forwarded-For头部的值由多个IP地址组成,以逗号分隔。例如,如果请求经过了三个代理服务器,则X-Forwarded-For头部的值为:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c">X-Forwarded-For: client_ip, proxy1_ip, proxy2_ip<br></code></pre></td></tr></table></figure><p>其中,client_ip代表客户端的真实IP地址,proxy1_ip和proxy2_ip分别代表两个中间代理服务器的IP地址。</p><p>从逻辑来讲来看,应该取第一个地址,因为如果在多级代理下,比如client -> F5 -> (A/B) ->server,同一个client可能会走A或者B,这就会导致hash值发生变化。简单搭个环境,打开debug测试一下,<strong>hash模块用的确实是真实客户端ip,也就是第一个ip</strong>,这样就是没有问题的。</p><p><img src="/img/socket-%E5%9B%BE17.png" alt="socket-图17"></p><p>有意思的是,如果客户端后的第一个节点没有获取到client_ip,第二个节点获取到了第一个节点的ip,就会导致X-Forwarded-For的client_ip始终是客户端后第一个节点的ip,ip固定了,hash直接就无效了,因为不管客户端再怎么变化,client_ip始终是第一个代理节点。所以要保证整个链路支持X-Forwarded-For。</p><p>事实证明,生产根本不是这样的,尤其是你的客户用了各种各样的负载均衡器,7层还好一般都支持,4层就够呛了(第一层代理节点要从tcp连接获取客户端的ip),如果客户买了F5,就一定会配置,或者愿意配置吗?</p><p>对比一下几种算法</p><table><thead><tr><th>功能/支持</th><th>使用真实客户端ip进行ip_hash</th><th>一致性哈希</th></tr></thead><tbody><tr><td>ip_hash</td><td>否</td><td>否</td></tr><tr><td>hash</td><td>是</td><td>是</td></tr><tr><td>sticky</td><td>否,使用cookie的router值路由</td><td>否</td></tr></tbody></table><p>1、使用不同的算法会有什么问题?</p><p>1、使用ip_hash代理,退化成单节点</p><p>如果停掉在用的,当前会话直接挂掉,nginx会重新选取一个节点。根据socket.io的特性,会出现报错,停掉空闲节点,不会有影响。</p><p>另外补充一个点,如果 client – nginx – 后端服务,这种模式下,nginx开启ip_hash后,如果停掉一个节点,nginx会重新计算权值,这个值影响最终请求被路由到哪台机器,也就是粘性会话失效了。</p><p>也就是ip_hash的2个缺点,<strong>集群退化单节点</strong>与<strong>不支持一致性哈希</strong></p><p>2、使用hash</p><p>首先肯定是使用真实客户端ip进行hash路由,这样可以避免使后端集群退化成单节点</p><p>而且现在因为后端服务是多节点,停止掉一个节点后,数据项的hash值会发生变化,客户端的请求会路由到其他的节点,且这个服务的数据丢失。</p><p>但是使用一致性哈希后,数据会同步到顺时针的下一个节点,整个集群不会因为增删节点,影响对外提供功能</p><p>以ingress-nginx举例:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs css">nginx<span class="hljs-selector-class">.ingress</span><span class="hljs-selector-class">.kubernetes</span><span class="hljs-selector-class">.io</span>/upstream-hash-by: <span class="hljs-string">"$http_x_forwarded_for"</span><br></code></pre></td></tr></table></figure><h2 id="8、实际问题"><a href="#8、实际问题" class="headerlink" title="8、实际问题"></a>8、实际问题</h2><h3 id="1、实际问题-1"><a href="#1、实际问题-1" class="headerlink" title="1、实际问题 1"></a>1、实际问题 1</h3><p>1、业务背景</p><p>某系统使用socket.io做数据推送,架构如下图,打开F12,这个前端同时存在polling和websocket的请求,有了上面的经验,这个很容易,我们在Nginx,针对socket.io的请求,配置<strong>会话保持</strong>和<strong>websocket升级</strong>的配置就可以,但是有问题,这主要是业务的使用方式。</p><p><img src="/img/socket-%E5%9B%BE18.png" alt="socket-图18"></p><p>2、 业务的使用</p><p>1、client首先会发送一个post请求,告知后台微服务,客户端想获取什么数据。</p><p>2、服务端组装好数据后,通过Socket.io将就绪的通知,通过websocket的请求推送到客户端。</p><p>3、客户端接收到数据就绪的通知后,再次发送一个get请求,下载数据 。</p><p>业务只是对socket.io的请求配置了ip_hash,那么试想,</p><p>1、如果客户端的websocket请求是和第一个Socket.io服务端建立的</p><p>2、post请求没有配置ip_hash,所以是轮询负载的,每2笔会有一笔发给了第二个Socket.io。</p><p>3、第二个Socket.io服务端收到了前端的请求,但是它没法通知客户端数据已经准备好了,因为没有websocket的连接,所以客户端一直不会发起下载的请求。</p><p>4、请求卡住了,导致整个系统卡住了</p><p>所以将这2个请求也配置为ip_hash,事实证明,问题解决。</p><p>3、 提问环节</p><p>本文一直没有提七层负载和四层负载的区别,刚好这里有个四层负载,那么有2个问题需要留给读者</p><p>1、上图的四层负载没有做任何改动,如果换成七层负载,需要加什么配置?</p><p>提示:对于polling的请求,上图的负载均衡器是四层和七层其实没区别(只限于这种架构,其他架构可能会有区别,具体问题具体分析),那么我们需要考虑的就是websocket协议了,可以参考另外一篇文章,《浅析nginx实现websocket原理》</p><p>2、哪个集群退化成了单点?</p><h3 id="2、实际问题-2"><a href="#2、实际问题-2" class="headerlink" title="2、实际问题 2"></a>2、实际问题 2</h3><p>为了建立一个WebSocket连接,客户端需要建立一个tcp连接并且发送一个握手协议。连接最初状态为CONNECTING,但是客户端最多有一条连接可以处于CONNECTING状态。如果多个连接尝试同时与一个相同的IP地址建立连接,客户端必须把他们进行排序。如果是web端,Chrome浏览器最多允许对同一个域名Host建立6个TCP连接,不同的浏览器有所区别。</p><p>因此某客户遇到了一个问题,使用的socket.io服务总是连接不成功,且浏览器会直接卡死,严重影响业务的正常运转。排查问题是要讲究步骤的,因此</p><p>1、首先,查看了现场的现象,发现浏览器卡死,且发送了很多socket.io的请求,刚好是6条,但是请求一直处于pending状态。</p><p>2、然后,确认了现场的架构拓扑,如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c">client -> F5 -> Nginx(集群) -> socket.io(集群)<br></code></pre></td></tr></table></figure><p>3、确认了F5的负载模式是7层负载,且没有配置websocket协议升级,Nginx倒是配置了会话保持(现场的实施根据部署文档进行配置),但是配置还存在问题。</p><p>4、如果确认了上面的信息,那么我们可以清晰的发现,问题解决很简单。先来解答一下异常现象</p><ul><li>为什么请求处于pengding状态?</li></ul><p>因为F5使用了7层负载,但是没有配置websocket协议升级,而客户端是同时存在polling和websocket请求的,所以客户端和F5之间的websocket请求一直是建立失败的,只是建立了HTTP1.1的连接,虽然也是长连接,但是属于HTTP,数据的传输格式不一样,报错是肯定的。但是先不要着急,现在还没到这个报错的时候,HTTP的请求如果一直没有响应,那么就是pengding的状态,所以这就是为什么请求都处于pengding的状态。</p><ul><li>浏览器为什么会卡死?</li></ul><p>如果websocket的连接建立不成功,它会一直重复发,直到浏览器的上限,6条tcp连接。</p><p>5、这个架构其实我们已经很熟了,上面也有例子,F5配置websocket协议升级,HSIAR配置会话保持+websocket协议升级即可,但是F5支不支持websocket呢?有版本限制,且配置比较复杂,客户不想研究,那么直接将7层负载变为4层负载,这样问题就解决了。</p>]]></content>
<tags>
<tag>nginx</tag>
<tag>websocket</tag>
<tag>socket.io</tag>
</tags>
</entry>
<entry>
<title>openresty---lua调用c原理分析</title>
<link href="/2024/04/12/openresty---lua%E8%B0%83%E7%94%A8c%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/"/>
<url>/2024/04/12/openresty---lua%E8%B0%83%E7%94%A8c%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/</url>
<content type="html"><![CDATA[<h1 id="openresty—lua调用c模块原理分析"><a href="#openresty—lua调用c模块原理分析" class="headerlink" title="openresty—lua调用c模块原理分析"></a>openresty—lua调用c模块原理分析</h1><h2 id="1、lua基础"><a href="#1、lua基础" class="headerlink" title="1、lua基础"></a>1、lua基础</h2><h3 id="1、lua虚拟机"><a href="#1、lua虚拟机" class="headerlink" title="1、lua虚拟机"></a>1、lua虚拟机</h3><p><code>lua</code>是解释型语言,需要虚拟机对象。不同的<code>lua</code>虚拟机之间的工作是线程安全的,因为一切和虚拟机相关的内存操作都被关联到虚拟机对象中,而没有利用任何其它共享变量。<code>lua</code>的虚拟机核心部分,没有任何的系统调用,是一个纯粹的黑盒子,正确的使用<code>lua</code>,不会对系统造成任何干扰。这其中最关键的一点是,<code>lua</code>让用户自行定义内存管理器,在创建<code>lua</code>虚拟机时传入,这保证了<code>lua</code>的整个运行状态是用户可控的。</p><h3 id="2、状态机"><a href="#2、状态机" class="headerlink" title="2、状态机"></a>2、状态机</h3><p><code>global_State</code>:全局状态机</p><p><code>lua_State</code>:协程状态机</p><p>从<code>lua</code>的使用者的角度看,<code>global_State</code>是不可见的。我们无法用公开的API取到它的指针,也不需要引用它。<code>global_State</code>里面有对主线程的引用,有注册表管理所有全局数据,有全局字符串表,有内存管理函数,有<code>GC</code>需要的把所有对象串联起来的相关信息,以及一切<code>lua</code>在工作时需要的工作内存。<br>通过<code>lua_newstate</code>创建一个新的<code>lua</code>虚拟机时,第一块申请的内存将用来保存主线程和这个全局状态机。<code>lua</code>的实现尽可能的避免内存碎片,同时也减少内存分配和释放的次数。它采用了一个小技巧,利用一个<code>LG</code>结构,把主线程<code>lua_State</code>和<code>global_State</code>分配在一起。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">LX</span> {</span><br><span class="hljs-meta">#<span class="hljs-keyword">if</span> defined ( luaI_EXTRASPACE )</span><br><span class="hljs-type">char</span> buff [ luaI_EXTRASPACE ];<br><span class="hljs-meta"># <span class="hljs-keyword">endif</span></span><br>lua_State l;<br>} LX;<br><br><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">LG</span> {</span><br>LX l;<br>global_State g;<br>} LG;<br></code></pre></td></tr></table></figure><p><code>lua_newstate</code>的实现</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs c">lua_API lua_State * <span class="hljs-title function_">lua_newstate</span> <span class="hljs-params">( lua_Alloc f, <span class="hljs-type">void</span> *ud)</span> {<br> <span class="hljs-type">int</span> i;<br> lua_State *L; <span class="hljs-comment">//创建一个主线程状态机</span><br> global_State *g; <span class="hljs-comment">//创建一个全局状态机</span><br> LG *l = cast (LG *, (*f)(ud , <span class="hljs-literal">NULL</span> , lua_TTHREAD , <span class="hljs-keyword">sizeof</span> (LG))); <span class="hljs-comment">//申请内存</span><br> ................................................................................<br><span class="hljs-keyword">return</span> L; <br>}<br></code></pre></td></tr></table></figure><h3 id="3、version"><a href="#3、version" class="headerlink" title="3、version"></a>3、version</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">void</span> <span class="hljs-title function_">luaL_checkversion</span> <span class="hljs-params">(lua_State *L)</span>;<br></code></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs c">lua_API <span class="hljs-type">const</span> lua_Number * <span class="hljs-title function_">lua_version</span> <span class="hljs-params">( lua_State *L)</span> {<br><span class="hljs-type">static</span> <span class="hljs-type">const</span> lua_Number version = lua_VERSION_NUM ;<br><span class="hljs-keyword">if</span> (L == <span class="hljs-literal">NULL</span> ) <span class="hljs-keyword">return</span> & version ;<br><span class="hljs-keyword">else</span> <span class="hljs-keyword">return</span> G(L) -> version ;<br>}<br></code></pre></td></tr></table></figure><p>检查调用它的内核是否是创建这个 <code>lua</code> 状态机的内核。以及调用它的代码是否使用了相同的 <code>lua</code> 版本。同时也检查调用它的内核与创建该 <code>lua</code> 状态机的内核是否使用了同一片地址空间。</p><ol><li><strong>检查调用它的内核是否是创建这个 <code>lua</code> 状态机的内核</strong>:假设你正在编写一个 <code>lua</code> 插件,这个插件将被加载到不同的 <code>lua</code> 程序中。这些程序可能使用了不同版本的 <code>lua</code> 内核。在这种情况下,你的插件需要确保它能在所有这些程序中正常工作。你可以在插件的初始化代码中调用 <code>luaL_checkversion</code> 来确保插件被加载的 <code>lua</code> 程序使用的是和插件编译时相同版本的 <code>lua</code> 内核。</li><li><strong>调用它的代码是否使用了相同的 <code>lua</code> 版本</strong>:假设你正在维护一个 <code>lua</code> 库,这个库被不同的项目使用,而这些项目可能使用了不同版本的<code>lua</code>。在这种情况下,你需要确保你的库在所有这些项目中都能正常工作。你可以在库的初始化代码中调用 <code>luaL_checkversion</code> 来确保使用库的项目使用的是和库编译时相同版本的 <code>lua</code>。</li><li><strong>检查调用它的内核与创建该 lua 状态机的内核是否使用了同一片地址空间</strong>:这通常发生在你的 <code>lua</code> 代码需要和其他语言(如 <code>C</code> 或 <code>C++</code>)的代码交互时。例如,你的 <code>lua</code> 代码调用了一个 <code>C</code> 函数,这个 <code>C</code> 函数创建了一个新的 <code>lua</code> 状态机,并尝试在这个新的状态机上执行一些 <code>lua</code> 代码。在这种情况下,你需要确保这个新的状态机和原来的状态机在同一片地址空间,否则可能会导致内存错误。你可以在 C 函数中调用 <code>luaL_checkversion</code> 来进行这个检查。</li></ol><h3 id="4、元表"><a href="#4、元表" class="headerlink" title="4、元表"></a>4、元表</h3><p><code>lua</code>语言的元表类似于<code>c++</code>的类与对象,c++的每个类都可以绑定成员函数、成员变量,还可以对成员方法进行重载等等,通过实例化一个对象,可以对对象进行一系列的操作。c++是面向对象的语言,当有一个函数需要共用,又不想对类进行继承,可以使用static关键字,定义为一个全局的函数。从这些外在表现的方面看,c++和lua其实很像,但是显然lua更加轻量</p><p>lua 中的每一个值都可以绑定一个元表。这个元表是一个普通的table,它可以定义与该值相关的某些操作。你可以通过设置元表中特定域的值来改变Lua 值的行为。比如当一个非数字型的值作为加法操作的操作数时,Lua 会检查该值是否绑定了元表并且元表设置了域“__add”的值为一个函数,如果是,那么Lua 就会调用这个函数来进行该值的加法操作。</p><p>每个table 和full userdata 类型的值都可以有自己单独的元表(但多个table 和userdata可以共享一个元表)。其它的每一种类型对应地只能绑定一个元表。也就是说,<strong>所有的数字类型只能绑定同一个元表</strong>,<strong>所有的字符串类型只能绑定同一个元表</strong>,等等。除了字符串类型的值默认有一个元表外,其它的值默认是没有元表的。</p><p>一个元表控制了一个对象的算术、比较、连接,取长度操作和索引操作的行为。原理可以去看《lua官方文档》,这里主要关注2个有意思的元方法,索引和赋值</p><ul><li><p>index:索引操作。当使用一个不存于table 中的键去索引table 中的内容时会尝试调用此元方法。(当索引操作作用于的对象不是一个table 时,那么所有键都是不存在的,所以元方法一定会被尝试调用。)</p></li><li><p>newindex: table 赋值操作table[key] = value。使用一个不存在于table 中的键来给table 中的域赋值时会尝试调用此元方法。</p></li></ul><p><code>index:</code></p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">gettable_event</span> <span class="hljs-params">(table, key)</span></span><br><span class="hljs-keyword">local</span> h<br><span class="hljs-keyword">if</span> <span class="hljs-built_in">type</span>(<span class="hljs-built_in">table</span>) == <span class="hljs-string">"table"</span> <span class="hljs-keyword">then</span><br><span class="hljs-keyword">local</span> v = <span class="hljs-built_in">rawget</span>(<span class="hljs-built_in">table</span>, key)<br><span class="hljs-comment">-- 如果键存在,返回原始的值</span><br><span class="hljs-keyword">if</span> v ~= <span class="hljs-literal">nil</span> <span class="hljs-keyword">then</span> <span class="hljs-keyword">return</span> v <span class="hljs-keyword">end</span><br>h = metatable(<span class="hljs-built_in">table</span>).<span class="hljs-built_in">__index</span><br><span class="hljs-keyword">if</span> h == <span class="hljs-literal">nil</span> <span class="hljs-keyword">then</span> <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span> <span class="hljs-keyword">end</span><br><span class="hljs-keyword">else</span><br>h = metatable(<span class="hljs-built_in">table</span>).<span class="hljs-built_in">__index</span><br><span class="hljs-keyword">if</span> h == <span class="hljs-literal">nil</span> <span class="hljs-keyword">then</span><br><span class="hljs-built_in">error</span>(···)<br><span class="hljs-keyword">end</span><br><span class="hljs-keyword">end</span><br><span class="hljs-keyword">if</span> <span class="hljs-built_in">type</span>(h) == <span class="hljs-string">"function"</span> <span class="hljs-keyword">then</span><br><span class="hljs-keyword">return</span> (h(<span class="hljs-built_in">table</span>, key)) <span class="hljs-comment">-- 调用元方法</span><br><span class="hljs-keyword">else</span> <span class="hljs-keyword">return</span> h[key] <span class="hljs-comment">-- 或者把元方法当作一个table 来使用</span><br><span class="hljs-keyword">end</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><p><code>newindex:</code></p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">settable_event</span> <span class="hljs-params">(table, key, value)</span></span><br><span class="hljs-keyword">local</span> h<br><span class="hljs-keyword">if</span> <span class="hljs-built_in">type</span>(<span class="hljs-built_in">table</span>) == <span class="hljs-string">"table"</span> <span class="hljs-keyword">then</span><br><span class="hljs-keyword">local</span> v = <span class="hljs-built_in">rawget</span>(<span class="hljs-built_in">table</span>, key)<br><span class="hljs-comment">-- 如果键存在,那就做原始赋值</span><br><span class="hljs-keyword">if</span> v ~= <span class="hljs-literal">nil</span> <span class="hljs-keyword">then</span> <span class="hljs-built_in">rawset</span>(<span class="hljs-built_in">table</span>, key, value); <span class="hljs-keyword">return</span> <span class="hljs-keyword">end</span><br>h = metatable(<span class="hljs-built_in">table</span>).<span class="hljs-built_in">__newindex</span><br><span class="hljs-keyword">if</span> h == <span class="hljs-literal">nil</span> <span class="hljs-keyword">then</span> <span class="hljs-built_in">rawset</span>(<span class="hljs-built_in">table</span>, key, value); <span class="hljs-keyword">return</span> <span class="hljs-keyword">end</span><br><span class="hljs-keyword">else</span><br>h = metatable(<span class="hljs-built_in">table</span>).<span class="hljs-built_in">__newindex</span><br> <span class="hljs-keyword">if</span> h == <span class="hljs-literal">nil</span> <span class="hljs-keyword">then</span><br><span class="hljs-built_in">error</span>(···)<br><span class="hljs-keyword">end</span><br><span class="hljs-keyword">end</span><br><span class="hljs-keyword">if</span> <span class="hljs-built_in">type</span>(h) == <span class="hljs-string">"function"</span> <span class="hljs-keyword">then</span><br>h(<span class="hljs-built_in">table</span>, key,value) <span class="hljs-comment">-- 调用元方法</span><br><span class="hljs-keyword">else</span> h[key] = value <span class="hljs-comment">--或者把元方法当作一个table 来使用</span><br><span class="hljs-keyword">end</span><br><span class="hljs-keyword">end</span><br></code></pre></td></tr></table></figure><h2 id="2、请求与lua-state的关系"><a href="#2、请求与lua-state的关系" class="headerlink" title="2、请求与lua_state的关系"></a>2、请求与lua_state的关系</h2><h3 id="1、lua-State是什么"><a href="#1、lua-State是什么" class="headerlink" title="1、lua_State是什么"></a>1、lua_State是什么</h3><p>在 <code>lua</code> 中,<code>lua_State</code> 是一个代表 lua 解释器状态的结构体指针,它包含了 lua 解释器的所有状态信息,例如当前的全局环境、栈状态等。可以把<code>lua_State</code> 理解为 lua 的一个线程或者执行环境。</p><p>在 lua 中,每个线程都有自己的独立的执行栈,局部变量,错误处理函数等。这些都被封装在 <code>lua_State</code> 结构体中。当在 lua 中创建一个新的线程(或者协程)时,lua 会为这个线程创建一个新的 <code>lua_State</code>。这个 <code>lua_State</code> 包含了这个线程的所有状态信息,使得这个线程可以独立于其他线程运行。这是 lua 中线程和协程实现的基础,也是 lua 能够支持并发编程的关键。</p><h3 id="2、openresty的协程"><a href="#2、openresty的协程" class="headerlink" title="2、openresty的协程"></a>2、openresty的协程</h3><p>lua 的协程(<code>coroutine</code>)是一种用户级的线程,它们不同于操作系统的线程,切换由程序自身控制,因此开销小,使用灵活。</p><p>在 OpenResty 中,lua 协程用于实现非阻塞 I/O。当一个请求需要进行 I/O 操作(如访问数据库)时,当前的 lua 协程会挂起,将控制权交给其他的协程。等到 I/O 操作完成后,原来的协程再恢复执行。这样,即使 I/O 操作是阻塞的,也不会影响到整个程序的执行。</p><h3 id="3、请求与协程的关系"><a href="#3、请求与协程的关系" class="headerlink" title="3、请求与协程的关系"></a>3、请求与协程的关系</h3><p>在 OpenResty 中,每个 worker 进程使用一个 lua VM(lua 虚拟机),并创建一个新的 <code>lua_State</code>(即主线程)来执行 lua 代码。当请求被分配到 worker 时,将在这个 lua VM 中创建一个协程,协程之间数据隔离,每个协程都具有独立的全局变量。</p><p>具体来讲,对于每个请求,Openresty都会创建一个协程来处理,<code>co = ngx_http_lua_new_thread(r, L, &co_ref);</code> 而这个创建的协程是系统协程,是主协程,用户无法控制它。而用户通过<code>ngx.thread.spawn</code>创建的协程是通过 <code>ngx_http_lua_coroutine_create_helper</code>创建出来的,用户创建的协程是主协程的子协程。并通过<code>ngx_http_lua_co_ctx_s</code>保存协程的相关信息。协程通过 <code>ngx_http_lua_run_thread</code> 函数来运行与调度,当前待执行的协程为 <code>ngx_http_lua_ctx_t->cur_co_ctx</code> 。</p><p>当 lua 代码调用 I/O 操作等异步接口时,<code>ngx_lua</code> 会挂起当前协程(并保护上下文数据),而不阻塞 worker 进程]。I/O 等异步操作完成时,<code>ngx_lua</code> 会恢复上下文,程序继续执行。这些操作对用户程序都是透明的,使得每个请求都在一个独立的 lua 线程中处理,各个请求之间互不影响,可以并发处理大量的请求,从而提高了系统的吞吐量。</p><h2 id="3、源码分析"><a href="#3、源码分析" class="headerlink" title="3、源码分析"></a>3、源码分析</h2><h3 id="1、lua与c模块的交互"><a href="#1、lua与c模块的交互" class="headerlink" title="1、lua与c模块的交互"></a>1、lua与c模块的交互</h3><p>1、预加载的注册方式,通常自己实现一个模块,采用这种方式</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs c">ngx_http_lua_add_package_preload<br><br> <br><span class="hljs-comment">//在OpenResty(基于Nginx的扩展)中,ngx_http_lua_add_package_preload 是一个用于预加载 lua 模块的函数。这个函数的主要作用是将 lua 模块预加载到 Nginx 工作进程的全局环境中,从而避免在每次请求时重新加载 lua 模块。</span><br><span class="hljs-comment">//具体而言,ngx_http_lua_add_package_preload 用于将 lua 模块与一个预定义的路径关联,以便在需要时可以快速地加载。这对于提高性能和减少模块加载时间非常有用,特别是在处理大量并发请求时。</span><br><span class="hljs-comment">//原型如下:</span><br> <br><span class="hljs-type">void</span> <span class="hljs-title function_">ngx_http_lua_add_package_preload</span><span class="hljs-params">(<span class="hljs-type">ngx_conf_t</span> *cf, <span class="hljs-type">const</span> <span class="hljs-type">char</span> *package, lua_CFunction func)</span>;<br><br><span class="hljs-comment">//cf: ngx_conf_t 结构,用于获取配置信息。</span><br><span class="hljs-comment">//package: lua 模块的名称,通常是点分隔的路径,例如 "resty.foo"。</span><br><span class="hljs-comment">//func: 一个 lua C 函数,用于加载并返回 lua 模块。这个函数在第一次加载模块时被调用,并且加载成功后,其返回值会被缓存,以便后续请求可以直接使用。</span><br></code></pre></td></tr></table></figure><p>如 <code>ngx_http_lua_upstream_module</code> 模块</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">ngx_int_t</span><br><span class="hljs-title function_">ngx_http_lua_upstream_init</span><span class="hljs-params">(<span class="hljs-type">ngx_conf_t</span> *cf)</span><br>{<br> <span class="hljs-keyword">if</span> (ngx_http_lua_add_package_preload(cf, <span class="hljs-string">"ngx.upstream"</span>,<br> ngx_http_lua_upstream_create_module)<br> != NGX_OK)<br> {<br> <span class="hljs-keyword">return</span> NGX_ERROR;<br> }<br><br> <span class="hljs-keyword">return</span> NGX_OK;<br>}<br></code></pre></td></tr></table></figure><p> <code>ngx_http_lua_add_package_preload</code>具体实现为:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">ngx_int_t</span><br><span class="hljs-title function_">ngx_http_lua_add_package_preload</span><span class="hljs-params">(<span class="hljs-type">ngx_conf_t</span> *cf, <span class="hljs-type">const</span> <span class="hljs-type">char</span> *package,</span><br><span class="hljs-params"> lua_CFunction func)</span><br>{<br> lua_State *L;<br> <span class="hljs-type">ngx_http_lua_main_conf_t</span> *lmcf;<br> <span class="hljs-type">ngx_http_lua_preload_hook_t</span> *hook;<br><br> lmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_lua_module);<br><br> L = lmcf->lua;<br> <br><span class="hljs-comment">//lua_getglobal(L, "package"): 获取全局变量 "package"。</span><br><span class="hljs-comment">//lua_getfield(L, -1, "preload"): 获取 "package" 表中的 "preload" 字段,这是一个用于存放预加载函数的表。</span><br><span class="hljs-comment">//lua_pushcfunction(L, func): 将 C 函数推入 lua 栈。</span><br><span class="hljs-comment">//lua_setfield(L, -2, package): 将 C 函数设置为 "preload" 表中的字段,字段名为 lua 模块的名称。</span><br><span class="hljs-comment">//lua_pop(L, 2): 弹出栈上的两个元素,即 "package" 表和 "preload" 表。</span><br><br><span class="hljs-comment">//很重要!!!!!*******//相当于建立了一个ngx.upstream的表,里面preload存放对应的函数----ngx_http_lua_upstream_module</span><br><br><br> <span class="hljs-keyword">if</span> (L) {<br> lua_getglobal(L, <span class="hljs-string">"package"</span>);<br> lua_getfield(L, <span class="hljs-number">-1</span>, <span class="hljs-string">"preload"</span>);<br> lua_pushcfunction(L, func);<br> lua_setfield(L, <span class="hljs-number">-2</span>, package);<br> lua_pop(L, <span class="hljs-number">2</span>);<br> }<br><br> <span class="hljs-comment">/* we always register preload_hooks since we always create new Lua VMs</span><br><span class="hljs-comment"> * when lua code cache is off. */</span><br><br> <span class="hljs-keyword">if</span> (lmcf->preload_hooks == <span class="hljs-literal">NULL</span>) {<br> lmcf->preload_hooks =<br> ngx_array_create(cf->pool, <span class="hljs-number">4</span>,<br> <span class="hljs-keyword">sizeof</span>(<span class="hljs-type">ngx_http_lua_preload_hook_t</span>));<br><br> <span class="hljs-keyword">if</span> (lmcf->preload_hooks == <span class="hljs-literal">NULL</span>) {<br> <span class="hljs-keyword">return</span> NGX_ERROR;<br> }<br> }<br><br> hook = ngx_array_push(lmcf->preload_hooks);<br> <span class="hljs-keyword">if</span> (hook == <span class="hljs-literal">NULL</span>) {<br> <span class="hljs-keyword">return</span> NGX_ERROR;<br> }<br><br> hook->package = (u_char *) package;<br> hook->loader = func;<br><br> <span class="hljs-keyword">return</span> NGX_OK;<br>}<br></code></pre></td></tr></table></figure><p><code>ngx_http_lua_upstream_create_module</code>的实现</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">int</span><br><span class="hljs-title function_">ngx_http_lua_upstream_create_module</span><span class="hljs-params">(lua_State * L)</span><br>{<br> lua_createtable(L, <span class="hljs-number">0</span>, <span class="hljs-number">6</span>);<br><br> lua_pushcfunction(L, ngx_http_lua_upstream_get_upstreams);<br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"get_upstreams"</span>);<br><br> lua_pushcfunction(L, ngx_http_lua_upstream_get_servers);<br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"get_servers"</span>);<br><br> lua_pushcfunction(L, ngx_http_lua_upstream_get_primary_peers);<br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"get_primary_peers"</span>);<br><br> lua_pushcfunction(L, ngx_http_lua_upstream_get_backup_peers);<br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"get_backup_peers"</span>);<br><br> lua_pushcfunction(L, ngx_http_lua_upstream_set_peer_down);<br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"set_peer_down"</span>);<br><br> lua_pushcfunction(L, ngx_http_lua_upstream_current_upstream_name);<br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"current_upstream_name"</span>);<br><br> <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>}<br></code></pre></td></tr></table></figure><p>2、非预加载的注册方式,openresty官方内置</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">ngx_int_t</span> <span class="hljs-title function_">ngx_http_lua_inject_xxx_api</span><span class="hljs-params">(lua_State *L)</span>{}<br></code></pre></td></tr></table></figure><p>如:<code>ngx_http_lua_inject_resp_header_api</code></p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_http_lua_inject_resp_header_api</span><span class="hljs-params">(lua_State *L)</span><br>{ <span class="hljs-comment">//创建一个新的lua表,并将其推入lua堆栈。这个表将用于存储HTTP响应头的键值对</span><br> lua_newtable(L); <span class="hljs-comment">/* .header */</span><br> <span class="hljs-comment">//创建一个新的lua表,并设置它的元表。元表是一个普通的lua表,它定义了一些特殊的操作,比如当在表中查找一个不存在的键时,会通过元表的__index元方法来获取值。在这里,我们为.header表创建了一个元表。</span><br> lua_createtable(L, <span class="hljs-number">0</span>, <span class="hljs-number">2</span>); <span class="hljs-comment">/* metatable for .header */</span><br> <br> <span class="hljs-comment">//将C函数ngx_http_lua_ngx_header_get推入堆栈,并将它作为值与键__index关联起来。这样,当在.header表中查找一个不存在的键时,将会调用ngx_http_lua_ngx_header_get函数来获取相应的值。</span><br> lua_pushcfunction(L, ngx_http_lua_ngx_header_get);<br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"__index"</span>);<br> <br> <span class="hljs-comment">//将C函数ngx_http_lua_ngx_header_set推入堆栈,并将它作为值与键__newindex关联起来。这样,当在.header表中设置一个不存在的键时,将会调用ngx_http_lua_ngx_header_set函数来设置相应的值。</span><br> lua_pushcfunction(L, ngx_http_lua_ngx_header_set);<br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"__newindex"</span>);<br> <br> <span class="hljs-comment">//将刚刚创建的元表设置为.header表的元表,从而实现了对HTTP响应头的读写操作。</span><br> lua_setmetatable(L, <span class="hljs-number">-2</span>);<br> <br> <span class="hljs-comment">//将.header表保存在全局环境中,命名为header,这样在lua脚本中可以通过ngx.header来访问和操作HTTP响应头。</span><br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"header"</span>);<br><br> lua_createtable(L, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>); <span class="hljs-comment">/* .resp */</span><br><br> lua_pushcfunction(L, ngx_http_lua_ngx_resp_get_headers);<br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"get_headers"</span>);<br> <br> <span class="hljs-comment">//ngx.resp</span><br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"resp"</span>);<br>}<br></code></pre></td></tr></table></figure><p>openresty在nginx的配置阶段统一注册</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">void</span><br><span class="hljs-title function_">ngx_http_lua_inject_ngx_api</span><span class="hljs-params">(lua_State *L, <span class="hljs-type">ngx_http_lua_main_conf_t</span> *lmcf,</span><br><span class="hljs-params"> <span class="hljs-type">ngx_log_t</span> *<span class="hljs-built_in">log</span>)</span><br>{<br> lua_createtable(L, <span class="hljs-number">0</span> <span class="hljs-comment">/* narr */</span>, <span class="hljs-number">115</span> <span class="hljs-comment">/* nrec */</span>); <span class="hljs-comment">/* ngx.* */</span><br><br> lua_pushcfunction(L, ngx_http_lua_get_raw_phase_context);<br> lua_setfield(L, <span class="hljs-number">-2</span>, <span class="hljs-string">"_phase_ctx"</span>);<br><br> ngx_http_lua_inject_arg_api(L);<br><br> ngx_http_lua_inject_http_consts(L);<br> ngx_http_lua_inject_core_consts(L);<br><br> ngx_http_lua_inject_resp_header_api(L); <span class="hljs-comment">//注册到线程中</span><br> <br> .......................................<br></code></pre></td></tr></table></figure><p>将lua与c代码关联起来,这样就可以在lua中调用ngx.header,比如:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c">local cookie = {}<br>ngx.header[<span class="hljs-string">"Set-cookie"</span>] = cookie<br></code></pre></td></tr></table></figure><p>3、关于__index</p><p>当尝试从表中获取不存在的值时,那么就会调用 <code>ngx_http_lua_ngx_header_get</code></p><p>在lua中,<code>__index</code> 是一种特殊的元方法(metamethod),用于表的访问控制。当你尝试从一个表中获取一个不存在的键时,lua会在表的元表中查找是否定义了<code>__index</code>元方法。如果找到了<code>__index</code>元方法,lua会调用它,并将表本身和要访问的键作为参数传递给该元方法。</p><p>在这段代码中,我们创建了一个名为 <code>.header</code> 的新表,并为该表创建了一个元表。然后,我们通过 <code>lua_setfield(L, -2, "__index")</code> 将名为 <code>__index</code> 的 C 函数(<code>ngx_http_lua_ngx_header_get</code>)与该元表中的 <code>__index</code> 键关联起来。这样,当在 <code>.header</code> 表中查找一个不存在的键时,lua 就会调用 <code>ngx_http_lua_ngx_header_get</code> 函数来获取相应的值。</p><p>换句话说,这个代码片段通过设置 <code>__index</code> 元方法,为 <code>.header</code> 表提供了一种自定义的行为:当访问 <code>.header</code> 表中不存在的键时,会调用 <code>ngx_http_lua_ngx_header_get</code> 函数进行处理。这在某种程度上实现了对 <code>.header</code> 表的动态访问控制。</p><h3 id="2、协程"><a href="#2、协程" class="headerlink" title="2、协程"></a>2、协程</h3><ol><li>nginx master初始化时,会创建一个lua_state,并初始化一个cached_lua_threads。 </li><li>master在fork work时,每个work会拥有各自的lua_state,即主协程</li><li>主协程会维护cached_lua_threads,存放这个work(也就是这个lua_state主协程)创建出的所有协程,可以重复使用。 </li><li>当有请求时,先检查 请求是否在这个虚拟机处理 && 协程队列是否为空 。 </li><li>如果满足条件,那么从队列取一个协程,绑定该请求的上下文 </li><li>如果不满足条件,说明此时没有主协程,或者没有可用的协程了,那就新建协程</li></ol><p>1、master进程初始化虚拟机,创建lua_state</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">//初始化ngx_http_lua_module模块 //初始化虚拟机,lmcf->lua为创建成功的虚拟机实例</span><br>ngx_http_lua_init -> rc = ngx_http_lua_init_vm(&lmcf->lua, <span class="hljs-literal">NULL</span>, cf->cycle, cf->pool, lmcf, cf-><span class="hljs-built_in">log</span>,<span class="hljs-literal">NULL</span>); <br></code></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">ngx_int_t</span><br><span class="hljs-title function_">ngx_http_lua_init_vm</span><span class="hljs-params">(lua_State **new_vm, lua_State *parent_vm,</span><br><span class="hljs-params"> <span class="hljs-type">ngx_cycle_t</span> *cycle, <span class="hljs-type">ngx_pool_t</span> *pool, <span class="hljs-type">ngx_http_lua_main_conf_t</span> *lmcf,</span><br><span class="hljs-params"> <span class="hljs-type">ngx_log_t</span> *<span class="hljs-built_in">log</span>, <span class="hljs-type">ngx_pool_cleanup_t</span> **pcln)</span><br>{<br> ..............................................<br><br> <span class="hljs-comment">/* create new lua VM instance */</span><br> L = ngx_http_lua_new_state(parent_vm, cycle, lmcf, <span class="hljs-built_in">log</span>); <span class="hljs-comment">//创建lua_state</span><br> <span class="hljs-keyword">if</span> (L == <span class="hljs-literal">NULL</span>) {<br> <span class="hljs-keyword">return</span> NGX_ERROR;<br> }<br><br> .....................................<br>}<br></code></pre></td></tr></table></figure><p>初始化协程队列</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">//初始化配置 //初始化队列</span><br>ngx_http_lua_init_main_conf -> ngx_queue_init(&lmcf->cached_lua_threads);<br></code></pre></td></tr></table></figure><p>2、lmcf->cached_lua_threads</p><p><code>lmcf->cached_lua_threads</code> 是一个队列,用于缓存 <strong>lua</strong> 协程(线程)。</p><ol><li>这个队列是在 <strong>Nginx</strong> 的 <strong>lua</strong> 模块中使用的,用于管理 <strong>lua</strong> 协程的生命周期。</li><li>具体作用包括但不限于:<ul><li>缓存已经创建的 <strong>lua</strong> 协程,以便在请求处理过程中重复使用。</li><li>避免频繁地创建和销毁协程,提高性能和效率。</li></ul></li><li>当需要执行 <strong>lua</strong> 脚本时,可以从这个队列中获取一个已经存在的协程,而不必每次都重新创建。</li></ol><p><code>lmcf->cached_lua_threads</code> 是一个用于缓存 <strong>lua</strong> 协程的队列,以优化请求处理性能</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><code class="hljs c">lua_State *<br><span class="hljs-title function_">ngx_http_lua_new_thread</span><span class="hljs-params">(<span class="hljs-type">ngx_http_request_t</span> *r, lua_State *L, <span class="hljs-type">int</span> *ref)</span><br>{<br> ................................<br><br> lmcf = ngx_http_get_module_main_conf(r, ngx_http_lua_module);<br><br> <span class="hljs-keyword">if</span> (L == lmcf->lua && !ngx_queue_empty(&lmcf->cached_lua_threads)) { <span class="hljs-comment">//L和lmcf->lua有可能不相等吗 && 协程队列不为空</span><br> q = ngx_queue_head(&lmcf->cached_lua_threads);<br> tref = ngx_queue_data(q, <span class="hljs-type">ngx_http_lua_thread_ref_t</span>, <span class="hljs-built_in">queue</span>);<br> } <span class="hljs-keyword">else</span> <span class="hljs-comment">//走到这里,说明 协程队列为空 </span><br> {<br> lua_pushlightuserdata(L, ngx_http_lua_lightudata_mask(<br> coroutines_key));<br> lua_rawget(L, lua_REGISTRYINDEX); <span class="hljs-comment">//从主协程获取线程队列</span><br> co = lua_newthread(L); <span class="hljs-comment">//新创建协程</span><br> lua_pushvalue(L, <span class="hljs-number">-1</span>); <span class="hljs-comment">//新创建的协程推入栈中</span><br> co_ref = luaL_ref(L, <span class="hljs-number">-3</span>); <span class="hljs-comment">//新协程的引用存储在注册表</span><br><br> ngx_log_debug2(NGX_LOG_DEBUG_HTTP, ngx_cycle-><span class="hljs-built_in">log</span>, <span class="hljs-number">0</span>,<br> <span class="hljs-string">"lua ref lua thread %p (ref %d)"</span>, co, co_ref);<br><br><span class="hljs-meta">#<span class="hljs-keyword">ifndef</span> OPENRESTY_luaJIT <span class="hljs-comment">//如果是jit,设置全局变量</span></span><br> <span class="hljs-keyword">if</span> (set_globals) {<br> lua_createtable(co, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>); <span class="hljs-comment">/* the new globals table */</span><br><br> <span class="hljs-comment">/* co stack: global_tb */</span><br><br> lua_createtable(co, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>); <span class="hljs-comment">/* the metatable */</span><br> ngx_http_lua_get_globals_table(co);<br> lua_setfield(co, <span class="hljs-number">-2</span>, <span class="hljs-string">"__index"</span>);<br> lua_setmetatable(co, <span class="hljs-number">-2</span>);<br><br> <span class="hljs-comment">/* co stack: global_tb */</span><br><br> ngx_http_lua_set_globals_table(co);<br> }<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br> }<br><br> ................................<br></code></pre></td></tr></table></figure><p>3、请求与协程创建关联的过程</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs c">ngx_http_lua_content_by_chunk(lua_State *L, <span class="hljs-type">ngx_http_request_t</span> *r)<br>{<br> ................................<br><br> <span class="hljs-comment">/* {{{ new coroutine to handle request */</span><br> co = ngx_http_lua_new_thread(r, L, &co_ref); <span class="hljs-comment">//主线程的创建</span><br><br> <span class="hljs-keyword">if</span> (co == <span class="hljs-literal">NULL</span>) {<br> ngx_log_error(NGX_LOG_ERR, r->connection-><span class="hljs-built_in">log</span>, <span class="hljs-number">0</span>,<br> <span class="hljs-string">"lua: failed to create new coroutine to handle request"</span>);<br><br> <span class="hljs-keyword">return</span> NGX_HTTP_INTERNAL_SERVER_ERROR;<br> } <br> ....................................<br>}<br></code></pre></td></tr></table></figure><h2 id="4、问题"><a href="#4、问题" class="headerlink" title="4、问题"></a>4、问题</h2><p>1、L和lmcf->lua有可能不相等吗?</p><ol><li><strong>多线程环境</strong>:如果你的应用程序在多线程环境中运行,每个线程可能有自己的 Lua 解释器状态。在这种情况下,如果 <code>L</code> 被设置为当前线程的 Lua 解释器状态,而 <code>lmcf->lua</code> 仍然引用主线程的 Lua 解释器状态,那么 <code>L == lmcf->lua</code> 就不会成立。</li><li><strong>Lua 解释器状态切换</strong>:在某些复杂的应用程序中,可能需要动态地切换 Lua 解释器状态。例如,一个请求可能需要在多个 Lua 解释器状态之间切换。在这种情况下,如果 <code>L</code> 被设置为当前需要的 Lua 解释器状态,而 <code>lmcf->lua</code> 仍然引用之前的 Lua 解释器状态,那么 <code>L == lmcf->lua</code> 就不会成立。</li><li><strong>Lua 解释器状态重新分配</strong>:如果 <code>L</code> 指向的 Lua 解释器状态被重新分配(例如,由于内存管理或垃圾收集),那么 <code>L == lmcf->lua</code> 就不会成立。</li></ol><p>以目前的认识来看,上述3种情况不会发生,这取决于openresty框架怎么设置L和lmcf->lua</p><blockquote><p>1、《lua源码剖析-云风》<br>2、<a href="https://segmentfault.com/a/1190000038878724">https://segmentfault.com/a/1190000038878724</a><br>3、openresty-1.25.3.1</p></blockquote>]]></content>
<tags>
<tag>lua虚拟机</tag>
<tag>global_State</tag>
<tag>lua_State</tag>
<tag>元表</tag>
<tag>协程</tag>
</tags>
</entry>
<entry>
<title>nginx的reuseport特性分析</title>
<link href="/2024/04/12/nginx%E7%9A%84reuseport%E7%89%B9%E6%80%A7%E5%88%86%E6%9E%90/"/>
<url>/2024/04/12/nginx%E7%9A%84reuseport%E7%89%B9%E6%80%A7%E5%88%86%E6%9E%90/</url>
<content type="html"><![CDATA[<h2 id="1、奇怪的现象"><a href="#1、奇怪的现象" class="headerlink" title="1、奇怪的现象"></a>1、奇怪的现象</h2><h3 id="1-1、断崖问题"><a href="#1-1、断崖问题" class="headerlink" title="1.1、断崖问题"></a>1.1、断崖问题</h3><p>业务进行性能测试,发现一个奇怪的现象,整个压测过程中总会有断崖的情况。本来TPS在2万8左右,会直接掉到1500左右,然后又马上恢复,但是只能恢复到2万2,损耗了20%。</p><p>现场架构:</p><p><img src="/img/%E7%A7%81%E6%9C%89%E5%8D%8F%E8%AE%AE%E5%8E%8B%E6%B5%8B.jpg" alt="私有协议压测"></p><p>现场XXX、nginx、服务都部署在一个机器,配置为:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs html">机器:海光麒麟v10 sp4 x86<br><br>性能指标:128c 512G<br><br>nginx配置:16个work进程,句柄数40960<br><br>客户端与nginx建立的长连接:16个<br></code></pre></td></tr></table></figure><p>问题在于nginx调大进程为64,或者客户端通道数调大为64,就没有问题。即16-64,64-16没问题,但是16-16有问题,100%复现断崖。</p><p>排查步骤:现场没有监控,所以排查过程比较困难,不过还是确认了一些问题</p><p>1、整个压测过程中,瓶颈不在nginx,因为最大cpu压力才到45%,主要排查问题是断崖。</p><p>2、查看nginx的日志,没有任何报错,但是发现断崖时,客户端给的流量下降了,根据日志绘出曲线后,与压力机的曲线一致</p><p>3、所以着重分析为什么此时客户端给的压力会突降低。但是整个链路的节点都有可能有问题,客户端队列阻塞?nginx处理变慢?服务端处理变慢?</p><p>重点是,这是私有协议,没有做access.log日志,根本看不到请求的耗时、数量等信息,从压力机看,64个进程和16个进程的平均时延没有差距。2中查看的流量还是打开debug,数的日志条数🙂,苦力活。因此开始tcpdump抓包,一个包抓了21个G,现场内网环境拷贝需要经过2层网络,现场还有其他的测试计划,只能见缝插针压测一把,第一天就这样过去了</p><p>第二天包终于拷贝出来了,在看包之前,我想起是信创系统,看了一下nginx的版本,不适配😬,赶快换了对应版本,16-16的模式没有再出现断崖,而且TPS上升了4千,到了3万2,一个数据库的分区直接被打满,断崖问题解决,但是因为操作系统不适配导致的断崖<strong>来日待查</strong>。紧急的问题是,压测的过程中,nginx的各个进程压力不均匀?进程的压力呈递减状态?</p><h3 id="1-2、每个work的连接数不均衡"><a href="#1-2、每个work的连接数不均衡" class="headerlink" title="1.2、每个work的连接数不均衡"></a>1.2、每个work的连接数不均衡</h3><p>我们拥有2种私有协议,可以理解为tcp+xxx数据格式、tcp+json。这类协议的客户端和服务端会建立一条长连接,通常成为通道,后续的请求都是依靠这个通道传输。</p><p>因此为了更加直观的复现,nginx开启了64个进程,xxx应用与nginx建立64条连接。使用netstat命令统计连接数,得以下结果,nginx共有64条连接(其中39个nginx进程有连接,25个进程处于空闲,没有连接。其中1个进程的连接数为4条;5个进程的连接数分别为3条;14个进程的连接数为2条;19个进程的连接数为1条;25个进程的连接数为0条;</p><p><img src="/img/nginx%E5%90%84%E4%B8%AA%E8%BF%9B%E7%A8%8B%E8%BF%9E%E6%8E%A5%E6%95%B0%E5%88%86%E5%B8%83%E5%9B%BE.png" alt="nginx各个进程连接数分布图"></p><p>压测过程中,发现共有39个nginx进程有压力,且压力大小与该进程数的连接数成正比,即nginx进程的连接数越多,压力越大,一个进程拿到了4个连接,一个进程拿到了1个连接,压力比是4:1。现场反馈的“递减”现象,其实就是这个现象。那么为什么连接数不一致?</p><p>2、HTTP协议的不均衡</p><p>既然私有协议的连接数不一致,那么来试试HTTP,直接使用HTTP协议连接nginx,配置做了更新,如下:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs html">128个work进程<br><br>keepalive_request默认值为100<br><br>服务每个增加到3个节点<br><br>数据库增加到3个分区<br></code></pre></td></tr></table></figure><p>架构如下:</p><p><img src="/img/HTTP%E7%9B%B4%E8%BF%9E.jpg" alt="HTTP直连"></p><p>发现nginx的压力依旧不均衡,只有十几个进程有压力,维持在60%~90%,此时tps已经达到8万+,依旧是数据库的瓶颈,到这里就需要研究nginx建立连接的机制。</p><h2 id="2、基础知识"><a href="#2、基础知识" class="headerlink" title="2、基础知识"></a>2、基础知识</h2><p>2.1、epoll</p><p>回顾一下4年前写的epoll的例子,<a href="https://github.com/ZJfans/EpollET-Server/blob/master/epollET.c">https://github.com/ZJfans/EpollET-Server/blob/master/epollET.c</a> </p><ul><li>初始化监听套接字</li><li>创建epoll实例</li><li>监听套接字设置到epoll_ctl中</li><li>使用epoll_wait,循环等待事件</li><li>如果触发事件的是监听套接字,那么建立新的连接</li><li>如果触发事件的是客户端的套接字,那么处理读写事件</li></ul><p>2.2、nginx的工作模式 区别于 muduo</p><p>nginx为多进程模式,初始化时,master监听端口,而后master fork多个work进程,此时所有的work监听同一端口</p><p>muduo为多线程的工作方式,main主线程负责处理监听套接字的事件,在建立连接后,将连接分配给work thread,后续的读写事件都由work线程处理</p><p><img src="/img/nginx%E5%90%84%E4%B8%AA%E8%BF%9B%E7%A8%8B%E7%BB%91%E5%AE%9A%E5%90%8C%E4%B8%80%E4%B8%AAsocket.png" alt="nginx各个进程绑定同一个socket"></p><h2 id="3、reuseport-SO-REUSEPORT"><a href="#3、reuseport-SO-REUSEPORT" class="headerlink" title="3、reuseport && SO_REUSEPORT"></a>3、reuseport && SO_REUSEPORT</h2><h3 id="3-1、惊群效应"><a href="#3-1、惊群效应" class="headerlink" title="3.1、惊群效应"></a>3.1、惊群效应</h3><p>惊群效应(Thundering Herd)是多进程或多线程系统在等待同一事件时可能遇到的问题。在网络编程中,尤其是在使用epoll进行I/O多路复用时,惊群效应可能导致性能问题。下面是关于Nginx如何处理惊群效应的详细解释。</p><p>原因</p><p>nginx的多个进程等待新的网络连接请求。当事件发生时(例如,一个新连接到达),所有等待的进程都会被唤醒。但最终只有一个进程能够处理该事件(例如,通过<code>accept</code>系统调用接受连接),其他进程在尝试处理事件失败后会重新进入等待状态。这个过程会导致大量的上下文切换和CPU资源的浪费。</p><p>Nginx采用了几种策略来避免或减少惊群效应的影响:</p><ol><li><strong><code>accept_mutex</code></strong>:<ul><li>Nginx可以使用<code>accept_mutex</code>来同步对<code>accept</code>调用的访问。这意味着在任何给定时间,只有一个工作进程可以处理新的连接请求。这通过在工作进程之间引入互斥锁来实现,从而避免了多个进程同时尝试接受同一个连接的情况。</li><li>通过在配置文件中设置<code>accept_mutex</code>为<code>on</code>,可以启用此功能。这有助于减少因多个进程竞争同一个<code>accept</code>操作而产生的惊群效应。</li></ul></li><li><strong><code>EPOLLEXCLUSIVE</code></strong>:<ul><li>从Linux内核版本4.5开始,引入了<code>EPOLLEXCLUSIVE</code>标志。Nginx从1.11.3版本开始支持这个特性。</li><li>当使用<code>EPOLLEXCLUSIVE</code>标志添加<code>epoll</code>事件时,内核保证在事件发生时只唤醒一个等待的进程。这减少了因多个进程监听同一个文件描述符而产生的惊群效应。</li></ul></li><li><strong><code>SO_REUSEPORT</code></strong>:<ul><li><code>SO_REUSEPORT</code>是Linux内核3.9版本引入的一个选项,允许多个进程绑定到相同的端口上。Nginx从1.9.1版本开始支持这个特性。</li><li>使用<code>SO_REUSEPORT</code>时,内核会在多个监听相同端口的进程之间进行负载均衡。这样,当新的连接请求到达时,内核会根据一定的规则选择一个进程来处理该请求,从而避免了多个进程同时被唤醒的问题。</li></ul></li></ol><h3 id="3-2、SO-REUSEPORT"><a href="#3-2、SO-REUSEPORT" class="headerlink" title="3.2、SO_REUSEPORT"></a>3.2、SO_REUSEPORT</h3><p><code>SO_REUSEPORT</code> 是一个 Linux 内核级别的套接字选项,它允许多个套接字(通常是监听套接字)绑定到相同的网络地址和端口上。这个特性在 Linux 3.9 版本中引入,主要用于解决多进程或多线程环境中的惊群效应问题。以下是 <code>SO_REUSEPORT</code> 的实现原理的详细解释:</p><p>传统的端口绑定</p><p>在 <code>SO_REUSEPORT</code> 出现之前,根据 POSIX 标准,一个网络端口在同一时间内只能被一个套接字绑定。如果有多个进程想要监听同一个端口,它们必须使用某种同步机制(如互斥锁)来协调对端口的访问,这可能会导致性能问题和复杂的编程模型。</p><p><code>SO_REUSEPORT</code> 的引入</p><p><code>SO_REUSEPORT</code> 选项的引入打破了这个限制,它允许多个套接字监听同一个端口,而不需要特殊的同步机制。当启用 <code>SO_REUSEPORT</code> 时,内核会在内部进行负载均衡,将到达的数据包分发给监听该端口的多个套接字。</p><p>实现原理</p><ol><li><strong>端口复用</strong>:<ul><li>当多个进程或线程的套接字启用了 <code>SO_REUSEPORT</code> 并绑定到同一个端口时,内核会为每个套接字创建一个独立的接收队列。</li><li>所有到达的数据包(例如,新的连接请求)都会根据某种负载均衡算法在这些队列之间进行分配。</li></ul></li><li><strong>负载均衡</strong>:<ul><li>内核使用一种负载均衡算法(通常是轮询或某种形式的哈希算法)来决定哪个套接字应该接收特定的连接请求。</li><li>这意味着即使多个进程在监听同一个端口,每个进程也只会接收到一部分的连接请求,而不是全部。</li></ul></li><li><strong>并发处理</strong>:<ul><li>由于每个进程都有自己的接收队列,它们可以并发地处理连接请求,而不会相互干扰。</li><li>这种方式显著减少了进程间的上下文切换和竞争,提高了系统的并发处理能力。</li></ul></li><li><strong>安全性和隔离</strong>:<ul><li>尽管多个套接字绑定到了同一个端口,但它们之间的通信是隔离的。每个套接字只能处理分配给它的数据包。</li><li>此外,<code>SO_REUSEPORT</code> 选项通常要求所有绑定到同一端口的套接字必须属于同一个用户,以避免潜在的安全问题。</li></ul></li></ol><h3 id="3-3、负载均衡算法"><a href="#3-3、负载均衡算法" class="headerlink" title="3.3、负载均衡算法"></a>3.3、负载均衡算法</h3><p>这是内核从监听的哈希表中查找匹配的套接字,关键函数是compute_score,会给每一个socket算一个权重值,有点类似于nginx的轮询,也是按照算法,得出同一个upstream下每个server的权重,最大的分配请求</p><p>但是当开启SO_REUSEPORT后,其实会直接调用<strong>inet_lookup_reuseport</strong>,这里直接选择socket,选择到就return了。具体分析见第4节。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-keyword">struct</span> sock *<span class="hljs-title function_">inet_lhash2_lookup</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> net *net,</span><br><span class="hljs-params"><span class="hljs-keyword">struct</span> inet_listen_hashbucket *ilb2,</span><br><span class="hljs-params"><span class="hljs-keyword">struct</span> sk_buff *skb, <span class="hljs-type">int</span> doff,</span><br><span class="hljs-params"><span class="hljs-type">const</span> __be32 saddr, __be16 sport,</span><br><span class="hljs-params"><span class="hljs-type">const</span> __be32 daddr, <span class="hljs-type">const</span> <span class="hljs-type">unsigned</span> <span class="hljs-type">short</span> hnum,</span><br><span class="hljs-params"><span class="hljs-type">const</span> <span class="hljs-type">int</span> dif, <span class="hljs-type">const</span> <span class="hljs-type">int</span> sdif)</span><br>{<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sock</span> *<span class="hljs-title">sk</span>, *<span class="hljs-title">result</span> =</span> <span class="hljs-literal">NULL</span>;<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">hlist_nulls_node</span> *<span class="hljs-title">node</span>;</span><br><span class="hljs-type">int</span> score, hiscore = <span class="hljs-number">0</span>;<br><br>sk_nulls_for_each_rcu(sk, node, &ilb2->nulls_head) {<br>score = compute_score(sk, net, hnum, daddr, dif, sdif);<br><span class="hljs-keyword">if</span> (score > hiscore) {<br>result = inet_lookup_reuseport(net, sk, skb, doff,<br> saddr, sport, daddr, hnum, inet_ehashfn);<br><span class="hljs-keyword">if</span> (result)<br><span class="hljs-keyword">return</span> result;<br><br>result = sk;<br>hiscore = score;<br>}<br>}<br><br><span class="hljs-keyword">return</span> result;<br>}<br></code></pre></td></tr></table></figure><h3 id="3-4、均衡吗?引发reuseport奇怪的现象"><a href="#3-4、均衡吗?引发reuseport奇怪的现象" class="headerlink" title="3.4、均衡吗?引发reuseport奇怪的现象"></a>3.4、均衡吗?引发reuseport奇怪的现象</h3><p>如果nginx有8个进程监听这个端口,为什么我觉得会很不均匀的分配连接呢?于是用了我们自己的nginx,不开启reuseport的情况下,多个进程都监听了一个socket,但是开启了reuseport后,8个进程每个进程都监听了8个socket??为什么不是8个进程各自监听自己的socket呢?<br>1、难道是内核版本太低了不支持?或者显示有问题?</p><p>我这个虚拟机的内核是3.1,确实低了,于是找了一个4.19的操作系统,也是这样?</p><p>那就不是linux内核的版本问题</p><p><img src="/img/openresty-1.15.8%E5%BC%80%E5%90%AFreuseport.png" alt="openresty-1.15.8开启reuseport"></p><p>2、nginx的版本的问题?</p><p>我用的是openresty-1.15.8版本,因此nginx的版本也是15.8,因此我下载了最新的openresty-1.25版本,重新编译启动后,结果如下图,work进程完全符合我的预期!!!</p><p><img src="/img/openresty-1.25.3%E5%BC%80%E5%90%AFreuseport.png" alt="openresty-1.25.3开启reuseport"></p><p>因此我去对比了2个版本的代码,发现新版本确实做了优化,<strong>会close多余的socket</strong>。</p><p>而且lsof -i出来的也只是绑定的意思,nginx1.15.8版本的nginx进程虽然绑定了多个socket,但是并没有监听每一个,也就是没有把每一个socket放到epoll里面</p><p>nginx-15</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">if</span> (NGX_HAVE_REUSEPORT)</span><br> <span class="hljs-keyword">if</span> (ls[i].reuseport && ls[i].worker != ngx_worker) {<br> <span class="hljs-keyword">continue</span>;<br> }<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br></code></pre></td></tr></table></figure><p>nginx-25</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">if</span> (NGX_HAVE_REUSEPORT)</span><br> <span class="hljs-keyword">if</span> (ls[i].reuseport && ls[i].worker != ngx_worker) {<br> ngx_log_debug2(NGX_LOG_DEBUG_CORE, cycle-><span class="hljs-built_in">log</span>, <span class="hljs-number">0</span>,<br> <span class="hljs-string">"closing unused fd:%d listening on %V"</span>,<br> ls[i].fd, &ls[i].addr_text);<br><br> <span class="hljs-keyword">if</span> (ngx_close_socket(ls[i].fd) == <span class="hljs-number">-1</span>) {<br> ngx_log_error(NGX_LOG_EMERG, cycle-><span class="hljs-built_in">log</span>, ngx_socket_errno,<br> ngx_close_socket_n <span class="hljs-string">" %V failed"</span>,<br> &ls[i].addr_text);<br> }<br><br> ls[i].fd = (<span class="hljs-type">ngx_socket_t</span>) <span class="hljs-number">-1</span>;<br><br> <span class="hljs-keyword">continue</span>;<br> }<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span><br></code></pre></td></tr></table></figure><p>问题是master为什么也绑定了4个socket?acceept事件来时,master也会触发?</p><p>事实上不用担心这个问题,因为master根本不会把这些socket放到epoll里面,所以永远不会触发。</p><p>那能不能删除绑定呢?</p><p>nginx的重启依赖于master fork work,我在想是不是master的socket不能丢掉,要不然reload的时候,重新创建socket,那之前的一些状态是不是就丢掉了?</p><p>或者停止时,要关掉socket,那么master需要知道当前打开的句柄数,我觉得这个怀疑是最合理的</p><p><strong>有兴趣待查</strong></p><h2 id="4、linux内核源码分析"><a href="#4、linux内核源码分析" class="headerlink" title="4、linux内核源码分析"></a>4、linux内核源码分析</h2><p>现在来看下linux内核是如何实现<code>SO_REUSEPORT</code>,Linux 内核版本 3.9 中引入了这个特性,所以我下载了2个版本的linux内核代码,目前广泛使用的4.19和最新的6.8</p><p>6.8的代码比较清晰直观,直接用ai生成注释😀</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">/*</span><br><span class="hljs-comment"> * 在特定的网络环境中,查找监听哈希桶中与给定条件匹配的套接字。</span><br><span class="hljs-comment"> * 此函数在持有RCU读锁时被调用,不会增加套接字的引用计数。</span><br><span class="hljs-comment"> *</span><br><span class="hljs-comment"> * 参数:</span><br><span class="hljs-comment"> * - net: 网络环境上下文。</span><br><span class="hljs-comment"> * - ilb2: 指向当前监听哈希桶的指针。</span><br><span class="hljs-comment"> * - skb: 数据包缓冲区,可用于查找过程中的某些计算。</span><br><span class="hljs-comment"> * - doff: 数据包中头部的偏移量。</span><br><span class="hljs-comment"> * - saddr: 源IP地址。</span><br><span class="hljs-comment"> * - sport: 源端口号。</span><br><span class="hljs-comment"> * - daddr: 目标IP地址。</span><br><span class="hljs-comment"> * - hnum: 目标端口号。</span><br><span class="hljs-comment"> * - dif: 发送接口索引。</span><br><span class="hljs-comment"> * - sdif: 源发送接口索引。</span><br><span class="hljs-comment"> *</span><br><span class="hljs-comment"> * 返回值:</span><br><span class="hljs-comment"> * - 查找到的套接字指针,如果没有找到匹配的套接字则返回NULL。</span><br><span class="hljs-comment"> */</span><br><span class="hljs-type">static</span> <span class="hljs-keyword">struct</span> sock *<span class="hljs-title function_">inet_lhash2_lookup</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> net *net,</span><br><span class="hljs-params"><span class="hljs-keyword">struct</span> inet_listen_hashbucket *ilb2,</span><br><span class="hljs-params"><span class="hljs-keyword">struct</span> sk_buff *skb, <span class="hljs-type">int</span> doff,</span><br><span class="hljs-params"><span class="hljs-type">const</span> __be32 saddr, __be16 sport,</span><br><span class="hljs-params"><span class="hljs-type">const</span> __be32 daddr, <span class="hljs-type">const</span> <span class="hljs-type">unsigned</span> <span class="hljs-type">short</span> hnum,</span><br><span class="hljs-params"><span class="hljs-type">const</span> <span class="hljs-type">int</span> dif, <span class="hljs-type">const</span> <span class="hljs-type">int</span> sdif)</span><br>{<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sock</span> *<span class="hljs-title">sk</span>, *<span class="hljs-title">result</span> =</span> <span class="hljs-literal">NULL</span>;<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">hlist_nulls_node</span> *<span class="hljs-title">node</span>;</span><br><span class="hljs-type">int</span> score, hiscore = <span class="hljs-number">0</span>;<br><br><span class="hljs-comment">// 遍历哈希桶中的所有套接字,计算每个套接字与目标匹配的得分</span><br>sk_nulls_for_each_rcu(sk, node, &ilb2->nulls_head) {<br>score = compute_score(sk, net, hnum, daddr, dif, sdif);<br><span class="hljs-keyword">if</span> (score > hiscore) {<br><span class="hljs-comment">// 尝试使用ReusePort特性更新结果套接字,如果成功则直接返回</span><br>result = inet_lookup_reuseport(net, sk, skb, doff,<br> saddr, sport, daddr, hnum, inet_ehashfn);<br><span class="hljs-keyword">if</span> (result)<br><span class="hljs-keyword">return</span> result;<br><br><span class="hljs-comment">// 更新最高得分及对应的套接字</span><br>result = sk;<br>hiscore = score;<br>}<br>}<br><br><span class="hljs-keyword">return</span> result;<br>}<br></code></pre></td></tr></table></figure><p>那么重点是2个地方</p><h3 id="4-1、compute-score"><a href="#4-1、compute-score" class="headerlink" title="4.1、compute_score"></a>4.1、compute_score</h3><h4 id="4-1-1、compute-score函数"><a href="#4-1-1、compute-score函数" class="headerlink" title="4.1.1、compute_score函数"></a>4.1.1、<strong>compute_score</strong>函数</h4><p>类似于nginx的轮询算法,算出权重/分数,根据 权重/分数 分发事件</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 计算套接字的得分</span><br><span class="hljs-comment"> * </span><br><span class="hljs-comment"> * 本函数用于根据给定的网络套接字、网络、目的网络地址、差异接口和源差异接口信息,计算套接字的得分。</span><br><span class="hljs-comment"> * 得分根据套接字的网络匹配、端口号匹配、IPv4/IPv6类型、绑定的设备接口和接收到的数据包的CPU等条件计算。</span><br><span class="hljs-comment"> * </span><br><span class="hljs-comment"> * @param sk 指向当前套接字的指针。</span><br><span class="hljs-comment"> * @param net 指向当前网络的指针。</span><br><span class="hljs-comment"> * @param hnum 当前套接字的端口号。</span><br><span class="hljs-comment"> * @param daddr 目的网络地址。</span><br><span class="hljs-comment"> * @param dif 当前套接字绑定的差异接口索引。</span><br><span class="hljs-comment"> * @param sdif 源差异接口索引。</span><br><span class="hljs-comment"> * @return 返回套接字的得分,匹配不成功返回-1。</span><br><span class="hljs-comment"> */</span><br><span class="hljs-type">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-type">int</span> <span class="hljs-title function_">compute_score</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> sock *sk, <span class="hljs-keyword">struct</span> net *net,</span><br><span class="hljs-params"><span class="hljs-type">const</span> <span class="hljs-type">unsigned</span> <span class="hljs-type">short</span> hnum, <span class="hljs-type">const</span> __be32 daddr,</span><br><span class="hljs-params"><span class="hljs-type">const</span> <span class="hljs-type">int</span> dif, <span class="hljs-type">const</span> <span class="hljs-type">int</span> sdif)</span><br>{<br><span class="hljs-type">int</span> score = <span class="hljs-number">-1</span>; <span class="hljs-comment">// 初始化得分为-1分</span><br><br><span class="hljs-comment">// 检查套接字所属的网络是否与指定的网络相同,端口号是否匹配,并且套接字不是IPv6 only类型</span><br><span class="hljs-keyword">if</span> (net_eq(sock_net(sk), net) && sk->sk_num == hnum &&<br>!ipv6_only_sock(sk)) {<br><span class="hljs-comment">// 检查套接字的接收地址是否与目的地址不同</span><br><span class="hljs-keyword">if</span> (sk->sk_rcv_saddr != daddr)<br><span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>; <span class="hljs-comment">// 如果不同,直接返回-1</span><br><br><span class="hljs-comment">// 检查套接字是否绑定到指定的设备接口,并且设备接口是否匹配差异接口</span><br><span class="hljs-keyword">if</span> (!inet_sk_bound_dev_eq(net, sk->sk_bound_dev_if, dif, sdif))<br><span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>; <span class="hljs-comment">// 如果不匹配,返回-1</span><br><br><span class="hljs-comment">// 根据套接字是否绑定了设备接口,给予1分或2分的奖励</span><br>score = sk->sk_bound_dev_if ? <span class="hljs-number">2</span> : <span class="hljs-number">1</span>;<br><br><span class="hljs-comment">// 如果套接字是IPv4类型,额外加1分</span><br><span class="hljs-keyword">if</span> (sk->sk_family == PF_INET)<br>score++;<br><span class="hljs-comment">// 如果一个socket上次处理它的数据包的CPU与当前CPU相同,额外加1分</span><br><span class="hljs-keyword">if</span> (READ_ONCE(sk->sk_incoming_cpu) == raw_smp_processor_id())<br>score++;<br>}<br><span class="hljs-keyword">return</span> score; <span class="hljs-comment">// 返回计算出的得分</span><br>}<br></code></pre></td></tr></table></figure><p>score是有3个地方会变化,连接会分发给哪个socket,那就是看4个socket哪个点不一样,导致分不一样,也就是4个nginx进程</p><p>1、检查套接字是否绑定到指定的设备接口,并且设备接口是否匹配差异接口</p><p>这个就是网卡,显然对于nginx的4个进程而言,我都是监听所有的网卡,所以这里4个进程的socket得分都是1,也就没有差异</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs javascript">server {<br> listen <span class="hljs-number">38088</span> reuseport;<br> server_name example.<span class="hljs-property">com</span>;<br><br> location / {<br> root /usr/share/nginx/html;<br> index index.<span class="hljs-property">html</span> index.<span class="hljs-property">htm</span>;<br> }<br>}<br></code></pre></td></tr></table></figure><p>2、 如果套接字是IPv4类型,额外加1分<br>这里我只考虑ipv4地址的场景,虽然我也监听了ipv6,因此这里4个进程的socket得分也都加1,没有差异</p><p>3、如果接收到的数据包的CPU与当前CPU相同,额外加1分</p><p>首先先到<strong>sock_reuseport.c</strong>模块,看下<strong>sk_incoming_cpu</strong>的定义</p><ol><li>**<code>sk_incoming_cpu</code>**: 在Linux内核中,<code>sk_incoming_cpu</code>是套接字结构中的一个字段,它记录了最近处理该套接字传入数据的CPU核心。当新的数据包到达时,操作系统会尝试将数据包分配给记录在<code>sk_incoming_cpu</code>中的CPU核心来处理,以此来优化性能。</li></ol><h4 id="4-1-2、CPU和套接字的关系"><a href="#4-1-2、CPU和套接字的关系" class="headerlink" title="4.1.2、CPU和套接字的关系"></a>4.1.2、CPU和套接字的关系</h4><ol><li><strong>数据包到达</strong>: 当一个网络数据包到达时,它首先被网络接口卡(NIC)捕获,并通过中断通知CPU。</li><li><strong>中断处理</strong>: CPU接收到中断后,操作系统的中断处理程序会捕获这个事件,并开始处理数据包。</li><li><strong>套接字绑定</strong>: 操作系统的网络栈会根据数据包的目标地址和端口号,决定将数据包发送到哪个套接字。如果一个套接字已经绑定到了特定的端口,那么所有到达该端口的数据包都会被发送到这个套接字。</li><li><strong>CPU亲和性</strong>: 在多核CPU系统中,操作系统可能会将特定的套接字或网络流量绑定到特定的CPU核心,这种做法称为CPU亲和性(CPU affinity)。这样做的目的是为了提高效率,因为:<ul><li><strong>缓存利用</strong>:如果套接字总是在同一个CPU核心上处理数据,相关的数据结构和状态信息更有可能保留在该核心的CPU缓存中,从而减少内存访问延迟。</li><li><strong>上下文切换</strong>:减少不同CPU核心之间的上下文切换,因为数据包的处理总是在同一个核心上进行。</li><li><strong>负载均衡</strong>:通过将不同的套接字或网络流量分配给不同的CPU核心,可以实现更好的负载均衡。</li></ul></li></ol><p>重要的是第一个网络包达到的时候,那就看看3次握手吧</p><ol><li><strong>客户端发送 SYN 包</strong>: 当客户端想要建立与服务端的 TCP 连接时,它会发送一个 SYN(同步)包给服务端,这个包包含客户端的初始序列号。</li><li><strong>服务端接收 SYN 包并创建 socket</strong>: 服务端收到客户端的 SYN 包后,会分配资源并创建一个用于与客户端通信的 socket,并为该连接分配一个序列号,同时为其分配缓冲区等资源。</li><li><strong>服务端发送 SYN-ACK 包</strong>: 接着,服务端会发送一个 SYN-ACK 包给客户端,该包中包含服务端的序列号以及确认号(即客户端序列号加一),表示服务端已经接收到了客户端的 SYN 包,并愿意建立连接。</li><li><strong>客户端接收 SYN-ACK 包并发送 ACK 包</strong>: 客户端收到服务端的 SYN-ACK 包后,会发送一个 ACK(确认)包给服务端,确认服务端的 SYN 包,并携带服务端的序列号加一的确认号。</li><li><strong>连接建立完成</strong>: 当服务端收到客户端发送的 ACK 包后,连接就建立完成了,服务端和客户端之间可以开始进行数据传输。</li></ol><p><strong>可以看到服务端在接收到客户端的 SYN 包后,会创建一个用于与客户端通信的 socket,这时候就会更新cpu了,也就是sk_incoming_cpu</strong>,下次这个cpu在分配连接的时候,会优先给这个cpu处理过的socket,也就是加一分</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// 如果一个socket上次处理它的数据包的CPU与当前CPU相同,额外加1分</span><br><span class="hljs-keyword">if</span> (READ_ONCE(sk->sk_incoming_cpu) == raw_smp_processor_id())<br>score++;<br></code></pre></td></tr></table></figure><h4 id="4-1-3、更新sk-incoming-cpu"><a href="#4-1-3、更新sk-incoming-cpu" class="headerlink" title="4.1.3、更新sk_incoming_cpu"></a>4.1.3、更新sk_incoming_cpu</h4><p>重要的是<strong>reuseport_update_incoming_cpu</strong>,如何设置和更新sk_incoming_cpu</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">void</span> <span class="hljs-title function_">reuseport_update_incoming_cpu</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> sock *sk, <span class="hljs-type">int</span> val)</span><br>{<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sock_reuseport</span> *<span class="hljs-title">reuse</span>;</span><br><span class="hljs-type">int</span> old_sk_incoming_cpu;<br><br><span class="hljs-comment">// 如果reuseport选项未启用,直接更新sk_incoming_cpu值。</span><br><span class="hljs-keyword">if</span> (unlikely(!rcu_access_pointer(sk->sk_reuseport_cb))) {<br>WRITE_ONCE(sk->sk_incoming_cpu, val);<br><span class="hljs-keyword">return</span>;<br>}<br><br><span class="hljs-comment">// 加锁以保护对reuseport相关资源的访问。</span><br>spin_lock_bh(&reuseport_lock);<br><br><span class="hljs-comment">// 在加锁保护下更新sk_incoming_cpu值,以避免并发问题。</span><br>old_sk_incoming_cpu = sk->sk_incoming_cpu;<br>WRITE_ONCE(sk->sk_incoming_cpu, val); <span class="hljs-comment">//这里做更新</span><br><br><span class="hljs-comment">// 安全地访问reuseport_cb,考虑了锁的依赖关系。</span><br>reuse = rcu_dereference_protected(sk->sk_reuseport_cb,<br> lockdep_is_held(&reuseport_lock));<br><br><span class="hljs-comment">// 如果reuseport_cb变为NULL,说明套接字已关闭,直接解锁退出。</span><br><span class="hljs-keyword">if</span> (!reuse)<br><span class="hljs-keyword">goto</span> out;<br><br><span class="hljs-comment">// 根据incoming_cpu值的正负变化,调整计数。</span><br><span class="hljs-keyword">if</span> (old_sk_incoming_cpu < <span class="hljs-number">0</span> && val >= <span class="hljs-number">0</span>)<br>__reuseport_get_incoming_cpu(reuse);<br><span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (old_sk_incoming_cpu >= <span class="hljs-number">0</span> && val < <span class="hljs-number">0</span>)<br>__reuseport_put_incoming_cpu(reuse);<br><br>out:<br><span class="hljs-comment">// 释放锁。</span><br>spin_unlock_bh(&reuseport_lock);<br>}<br></code></pre></td></tr></table></figure><p>理解了sk_incoming_cpu,其实就可以理解得分,但是事实上开启了SO_REUSEPORT后,选择的函数是<strong>inet_lookup_reuseport</strong>。</p><h3 id="4-2、inet-lookup-reuseport"><a href="#4-2、inet-lookup-reuseport" class="headerlink" title="4.2、inet_lookup_reuseport"></a>4.2、inet_lookup_reuseport</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// 尝试使用ReusePort特性更新结果套接字,如果成功则直接返回</span><br>result = inet_lookup_reuseport(net, sk, skb, doff,<br> saddr, sport, daddr, hnum, inet_ehashfn);<br><span class="hljs-keyword">if</span> (result)<br><span class="hljs-keyword">return</span> result;<br></code></pre></td></tr></table></figure><p>得分完,如果找到了socket,那就直接返回了,让我们看下<strong>inet_lookup_reuseport</strong>做了什么</p><h4 id="4-2-1、inet-lookup-reuseport源码"><a href="#4-2-1、inet-lookup-reuseport源码" class="headerlink" title="4.2.1、inet_lookup_reuseport源码"></a>4.2.1、inet_lookup_reuseport源码</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">struct</span> sock *<span class="hljs-title function_">inet_lookup_reuseport</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> net *net, <span class="hljs-keyword">struct</span> sock *sk,</span><br><span class="hljs-params"> <span class="hljs-keyword">struct</span> sk_buff *skb, <span class="hljs-type">int</span> doff,</span><br><span class="hljs-params"> __be32 saddr, __be16 sport,</span><br><span class="hljs-params"> __be32 daddr, <span class="hljs-type">unsigned</span> <span class="hljs-type">short</span> hnum,</span><br><span class="hljs-params"> <span class="hljs-type">inet_ehashfn_t</span> *ehashfn)</span><br>{<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sock</span> *<span class="hljs-title">reuse_sk</span> =</span> <span class="hljs-literal">NULL</span>; <span class="hljs-comment">/* 默认返回NULL,表示没有找到可重用的套接字 */</span><br>u32 phash;<br><br><span class="hljs-comment">/* 如果当前套接字允许端口复用,则计算哈希值并尝试选择一个可重用的套接字 */</span><br><span class="hljs-keyword">if</span> (sk->sk_reuseport) {<br><span class="hljs-comment">/* 根据提供的函数指针调用相应的哈希函数计算端口哈希值 */</span><br>phash = INDIRECT_CALL_2(ehashfn, udp_ehashfn, inet_ehashfn,<br>net, daddr, hnum, saddr, sport);<br><span class="hljs-comment">/* 使用计算得到的哈希值从哈希表中选择一个合适的套接字 */</span><br>reuse_sk = reuseport_select_sock(sk, phash, skb, doff);<br>}<br><span class="hljs-keyword">return</span> reuse_sk;<br>}<br></code></pre></td></tr></table></figure><p>最后是调用了<strong>reuseport_select_sock</strong></p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * reuseport_select_sock - 选择合适的socket进行复用</span><br><span class="hljs-comment"> * @sk: 当前的socket结构体</span><br><span class="hljs-comment"> * @hash: 数据包的哈希值</span><br><span class="hljs-comment"> * @skb: 数据包的缓冲区</span><br><span class="hljs-comment"> * @hdr_len: 数据包头的长度</span><br><span class="hljs-comment"> * </span><br><span class="hljs-comment"> * 此函数根据给定的条件(如BPF程序的结果或哈希值)从复用端口的socket池中选择一个合适的socket。</span><br><span class="hljs-comment"> * 如果有配置的BPF程序,则会先尝试使用BPF程序来决定选择哪个socket。</span><br><span class="hljs-comment"> * 若无BPF程序或BPF程序决策失败,则会基于哈希值来选择socket。</span><br><span class="hljs-comment"> * </span><br><span class="hljs-comment"> * 返回值: 返回选择的socket结构体指针。如果没有合适的socket,则返回NULL。</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">struct</span> sock *<span class="hljs-title function_">reuseport_select_sock</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> sock *sk,</span><br><span class="hljs-params"> u32 hash,</span><br><span class="hljs-params"> <span class="hljs-keyword">struct</span> sk_buff *skb,</span><br><span class="hljs-params"> <span class="hljs-type">int</span> hdr_len)</span><br>{<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sock_reuseport</span> *<span class="hljs-title">reuse</span>;</span><br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">bpf_prog</span> *<span class="hljs-title">prog</span>;</span><br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sock</span> *<span class="hljs-title">sk2</span> =</span> <span class="hljs-literal">NULL</span>;<br>u16 socks;<br><br>rcu_read_lock();<br>reuse = rcu_dereference(sk->sk_reuseport_cb);<br><br><span class="hljs-comment">/* 如果内存分配失败或添加调用尚未完成,则直接退出 */</span><br><span class="hljs-keyword">if</span> (!reuse)<br><span class="hljs-keyword">goto</span> out;<br><br>prog = rcu_dereference(reuse->prog);<br>socks = READ_ONCE(reuse->num_socks);<br><span class="hljs-keyword">if</span> (likely(socks)) {<br><span class="hljs-comment">/* 配合__reuseport_add_sock()中的smp_wmb()使用 */</span><br>smp_rmb();<br><br><span class="hljs-comment">/* 如果没有配置BPF程序或者skb为空,则直接进行哈希选择 */</span><br><span class="hljs-keyword">if</span> (!prog || !skb)<br><span class="hljs-keyword">goto</span> select_by_hash;<br><br><span class="hljs-comment">/* 根据BPF程序类型执行相应的程序逻辑 */</span><br><span class="hljs-keyword">if</span> (prog->type == BPF_PROG_TYPE_SK_REUSEPORT)<br>sk2 = bpf_run_sk_reuseport(reuse, sk, prog, skb, <span class="hljs-literal">NULL</span>, hash);<br><span class="hljs-keyword">else</span><br>sk2 = run_bpf_filter(reuse, socks, prog, skb, hdr_len);<br><br>select_by_hash:<br><span class="hljs-comment">/* 如果没有使用BPF程序或BPF程序结果无效,则回退到使用哈希值选择socket */</span><br><span class="hljs-keyword">if</span> (!sk2)<br>sk2 = reuseport_select_sock_by_hash(reuse, hash, socks);<br>}<br><br>out:<br>rcu_read_unlock();<br><span class="hljs-keyword">return</span> sk2;<br>}<br></code></pre></td></tr></table></figure><p>实际的选择</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * reuseport_select_sock_by_hash - 根据哈希值选择一个合适的socket</span><br><span class="hljs-comment"> * @reuse: 指向reuseport结构的指针,包含要搜索的socket数组</span><br><span class="hljs-comment"> * @hash: 用于选择socket的哈希值</span><br><span class="hljs-comment"> * @num_socks: socket数组中的socket数量</span><br><span class="hljs-comment"> * </span><br><span class="hljs-comment"> * 描述:</span><br><span class="hljs-comment"> * 此函数用于在给定的socket数组中,根据特定的哈希值选择一个处于TCP_ESTABLISHED状态的socket。</span><br><span class="hljs-comment"> * 如果没有处于该状态的socket,则返回第一个找到的非TCP_ESTABLISHED状态的socket。</span><br><span class="hljs-comment"> * </span><br><span class="hljs-comment"> * 返回值:</span><br><span class="hljs-comment"> * 返回一个指向选择的socket的指针。如果没有找到合适的socket,则返回NULL。</span><br><span class="hljs-comment"> */</span><br><span class="hljs-type">static</span> <span class="hljs-keyword">struct</span> sock *<span class="hljs-title function_">reuseport_select_sock_by_hash</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> sock_reuseport *reuse,</span><br><span class="hljs-params"> u32 hash, u16 num_socks)</span><br>{<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sock</span> *<span class="hljs-title">first_valid_sk</span> =</span> <span class="hljs-literal">NULL</span>; <span class="hljs-comment">/* 用于存储第一个找到的有效(非TCP_ESTABLISHED)socket */</span><br><span class="hljs-type">int</span> i, j;<br><br>i = j = reciprocal_scale(hash, num_socks); <span class="hljs-comment">/* 使用哈希值和socket数量计算起始索引 */</span><br><span class="hljs-keyword">do</span> {<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sock</span> *<span class="hljs-title">sk</span> =</span> reuse->socks[i]; <span class="hljs-comment">/* 获取当前索引位置的socket */</span><br><br><span class="hljs-comment">/* 如果socket状态不是TCP_ESTABLISHED,则进行进一步判断 */</span><br><span class="hljs-keyword">if</span> (sk->sk_state != TCP_ESTABLISHED) {<br><span class="hljs-comment">/* 如果没有设置incoming_cpu,表示没有活动的连接请求,则返回当前socket */</span><br><span class="hljs-keyword">if</span> (!READ_ONCE(reuse->incoming_cpu))<br><span class="hljs-keyword">return</span> sk;<br><br><span class="hljs-comment">/* 如果当前socket的incoming_cpu与当前CPU一致,表示有活动的连接请求,则返回当前socket */</span><br><span class="hljs-keyword">if</span> (READ_ONCE(sk->sk_incoming_cpu) == raw_smp_processor_id())<br><span class="hljs-keyword">return</span> sk;<br><br><span class="hljs-comment">/* 如果还没有找到第一个有效的socket,则将当前socket设置为第一个有效socket */</span><br><span class="hljs-keyword">if</span> (!first_valid_sk)<br>first_valid_sk = sk;<br>}<br><br>i++; <span class="hljs-comment">/* 移动到下一个socket */</span><br><span class="hljs-keyword">if</span> (i >= num_socks)<br>i = <span class="hljs-number">0</span>; <span class="hljs-comment">/* 如果超出范围,则从头开始 */</span><br>} <span class="hljs-keyword">while</span> (i != j); <span class="hljs-comment">/* 如果当前索引与起始索引不同,继续循环 */</span><br><br><span class="hljs-keyword">return</span> first_valid_sk; <span class="hljs-comment">/* 返回第一个有效的socket,如果没有找到则返回NULL */</span><br>}<br></code></pre></td></tr></table></figure><p>reuse->socks[i],是一个指针数组,它存储了一系列 <code>struct sock</code> 指针。每个 <code>struct sock</code> 指针代表一个网络套接字,这些套接字都绑定到了同一个端口上,并且启用了 <code>SO_REUSEPORT</code> 特性。</p><p><code>num_socks;</code> 字段表示 <code>socks</code> 数组中当前有效的套接字(<code>struct sock</code> 指针)的数量。这个字段用于跟踪监听同一个端口并启用了 <code>SO_REUSEPORT</code> 特性的套接字数量。</p><h4 id="4-2-2、总结"><a href="#4-2-2、总结" class="headerlink" title="4.2.2、总结"></a>4.2.2、总结</h4><ol><li>根据<code>net</code>、<code>daddr</code>、<code>hnum</code>、<code>saddr</code> 和 <code>sport</code> 这几个参数计算一个hash值</li><li>使用哈希值和socket数量计算<strong>reuse->socks</strong>数组的起始索引</li><li>判断当前socket是否有连接请求在处理,如果没有,说明这个监听socket目前空闲,所以选择这个</li><li>如果上面没有返回,再判断<strong>sk_incoming_cpu</strong>,如果这个socket的上一次数据是当前cpu处理的,那么就选这个socket</li><li>如果这个socket不满足条件,那么作为保底,将这个socket设置为保底选择</li><li>循环3-5步骤</li><li>遍历完<strong>reuse->socks</strong>数组中的socket后,返回第一个有效的socket,如果没有找到则返回NULL</li></ol><h2 id="5、总结"><a href="#5、总结" class="headerlink" title="5、总结"></a>5、总结</h2><h3 id="5-1、原理总结"><a href="#5-1、原理总结" class="headerlink" title="5.1、原理总结"></a>5.1、原理总结</h3><p>对于内核而言,整个过程处于传输层,它不需要关注应用层,因此对于连接的分配,会最大化的优化处理速度,只考虑传输层的属性。主要点在于优先使用空闲的监听socket,并且使监听socket尽量在一个cpu处理,这样有利于cpu缓存的利用。</p><p>因此当开启<code>SO_REUSEPORT</code> 特性后,一个socket是否能拿到连接,取决于3个点</p><p>1、根据哈希值和socket数量计算<strong>reuse->socks</strong>数组的起始值是多少,第一个当然有优先优势</p><p>2、取决于当前socket是否处于空闲</p><p>3、上一次处理这个socket的数据的cpu,是否是当前cpu</p><h3 id="5-2、均匀吗?"><a href="#5-2、均匀吗?" class="headerlink" title="5.2、均匀吗?"></a>5.2、均匀吗?</h3><h4 id="5-1-1、不会绝对均匀"><a href="#5-1-1、不会绝对均匀" class="headerlink" title="5.1.1、不会绝对均匀"></a>5.1.1、不会绝对均匀</h4><p>当同一个客户端和同一个nginx建立64条长连接时,上面1中的<strong>哈希值和socket数量</strong>是一样的,所以<strong>数组的起始下标</strong>是一样的。</p><p>那么连接分配给哪个进程就取决于2、3。当64条连接<strong>绝对同时</strong>来临时,且nginx的socket此时并没有其它连接来时,也就是处于空闲时,那么第2步会保证每个socket拿到1条连接,但是问题是绝对同时?还要保证没有连接到这些socket,这是不可能的。</p><p>因为连接总会有先后时间,即数据包会先后到,第一个socket处理完第一个连接后,它就又处于空闲了,所以它还会拿到连接,没办法它有优势,起始值算的。</p><h4 id="5-1-2、会发生极限场景吗"><a href="#5-1-2、会发生极限场景吗" class="headerlink" title="5.1.2、会发生极限场景吗"></a>5.1.2、会发生极限场景吗</h4><p>nginx开启64个进程,只有几个进程能拿到连接?</p><p><strong>可能性几乎为0</strong>,因为连接虽然有先后,但是时间差会非常小,所以都会在2中分发。除非客户端隔一段时间发一个请求,事实上客户端如果建立连接会”同时”发的,但是因为有时间差,前面的socket会拿到更多的连接</p><p>同时当连接数量级足够大,那么会近似均匀,但是当只有几十个连接时,也会是近似均匀,但是看着差距会比较大,毕竟有的socket拿不到连接,也就是nginx的进程拿不到连接</p><h4 id="5-1-3、如果想在应用层保证连接数均匀可以实现吗"><a href="#5-1-3、如果想在应用层保证连接数均匀可以实现吗" class="headerlink" title="5.1.3、如果想在应用层保证连接数均匀可以实现吗"></a>5.1.3、如果想在应用层保证连接数均匀可以实现吗</h4><p>这是一个非常意思的想法,我们从2点考虑,可行性与性能。</p><p>1、可行性</p><p> 最直接的想法,应用层怎么判断进程现在拥有多少个长连接,以及长连接就是T2/T3,而不是websocket这种长连接?</p><p>2、性能</p><p>如果每次建立连接都需要判断是否是长连接,且均匀分发到各个进程,性能会断崖式下降</p><p><strong>结论:所以不可能做的到,也没有意义。</strong></p><h4 id="5-1-4、目前这种情况,有必要做连接的均匀分发吗"><a href="#5-1-4、目前这种情况,有必要做连接的均匀分发吗" class="headerlink" title="5.1.4、目前这种情况,有必要做连接的均匀分发吗"></a>5.1.4、目前这种情况,有必要做连接的均匀分发吗</h4><p> 依据上次实际的统计来看,64个进程会有39个进程拿到连接,也就是39个进程会工作。</p><p>所以cpu只会利用39个?</p><p>根本不是的,因为work进程使用cpu是会切换的,这也是压测到极限,cpu的利用率会超过100%,有些java服务甚至会达到几千。因此cpu的利用率,nginx是最大化的,只不过存在cpu切换的损耗,基本可以忽略不计。</p><p> 所以,当压力到达nginx极限时,不同的进程的cpu利用率会有不同,但是一定会利用到所有的cpu,也就是可以发挥机器的最大性能。</p>]]></content>
<tags>
<tag>nginx</tag>
<tag>reuseport</tag>
<tag>linux内核</tag>
<tag>SO_REUSEPORT</tag>
</tags>
</entry>
<entry>
<title>Cookie属性之secure、httponly</title>
<link href="/2024/04/12/Cookie%E5%B1%9E%E6%80%A7%E4%B9%8Bsecure%E3%80%81httponly/"/>
<url>/2024/04/12/Cookie%E5%B1%9E%E6%80%A7%E4%B9%8Bsecure%E3%80%81httponly/</url>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>Cookie是一种用于在Web浏览器和Web服务器之间传递信息的机制,具有多种属性。经常会有安全测试不了解Cookie的属性,而认为某个属性是漏洞,最常见的就是secure,作者就见过很几次漏洞报告,认为http协议下,Cookie的secure为false是一个安全漏洞,这其实是测试没有理解secure的真正作用。那么阅读本文你将得到以下几个问题的答案</p><p>1、Cookie在会话鉴权中扮演什么角色?</p><p>2、secure、httponly的作用是什么?</p><p>3、http协议secure为false,到底是不是漏洞?</p><h2 id="1、首先来了解Cookie的作用"><a href="#1、首先来了解Cookie的作用" class="headerlink" title="1、首先来了解Cookie的作用"></a>1、首先来了解Cookie的作用</h2><p>Cookie通常被用于存储用户的会话信息、个人偏好设置和其他重要的数据。通过在浏览器中存储小型数据文件的方式,允许Web应用程序在浏览器中存储和检索数据。以下是Cookie的一些常见用途:</p><ol><li>记住用户登录状态:当用户通过用户名和密码进行登录时,服务器会创建一个Cookie,用于记录用户的登录状态。在用户下次访问网站时,Web应用程序可以读取Cookie中的信息,以确认用户已经登录,然后将其自动重定向到其上一次访问的页面。</li><li>存储用户偏好设置:Web应用程序可以使用Cookie来存储用户的偏好设置,例如语言偏好、字体大小、主题颜色等。这样,在用户再次访问网站时,他们的偏好设置就可以自动应用,提高了用户体验。</li><li>跟踪用户活动:通过Cookie,Web应用程序可以追踪用户的活动,例如他们在网站上浏览的页面、使用的功能和购物车内容等。这些信息可以用于分析用户行为、个性化推荐、广告定向等。</li><li>收集统计数据:Cookie也可以用于收集访问网站的用户数量、浏览器类型、设备类型等统计数据。这些数据可以帮助网站优化性能、改进用户体验和制定营销策略。</li></ol><p>总的来说,Cookie是Web应用程序中不可或缺的一部分,它们帮助实现了许多重要的功能,从而提高了用户体验和Web应用程序的效果。</p><p>我们的产品基于openresty开发,作为互联网接入路由网关,具备会话鉴权的功能。登录时,网关会生成token等会话信息,设置到响应的Cookie头部,返回给浏览器,浏览器会在application存储Cookie信息,当有同域的请求发起时,浏览器会将此域的Cookie(如果有的话)携带并发往服务端进行认证;登出时,网关会设置一个空的Cookie返回给浏览器,相当于删除了application存储Cookie信息。下面为使用https协议登录时的交互:</p><ul><li>登录时</li></ul><p><img src="/img/image-20230305161709819.png" alt="image-20230305161709819"></p><ul><li>登出时:</li></ul><p><img src="/img/image-20230305161752487.png" alt="image-20230305161752487"></p><h2 id="2、Secure与HTTPOnly属性"><a href="#2、Secure与HTTPOnly属性" class="headerlink" title="2、Secure与HTTPOnly属性"></a>2、Secure与HTTPOnly属性</h2><p>由于Cookie的特殊性质,它们也成为了网络攻击的主要目标之一。在这种情况下,secure和httponly属性成为了确保Cookie安全的重要手段。</p><ol><li>Secure属性是Cookie属性的一种,它用于确保Cookie只在通过安全协议(如HTTPS)的情况下传输。如果将Cookie设置为secure,则只有在使用HTTPS时才会将Cookie发送到服务器,即使用HTTPS协议进行登录,但是后续的请求为HTTP,这样是无法将Cookie携带到服务端的。而且即使攻击者截取了用户的Cookie,也无法使用它们进行会话劫持等攻击。</li><li>HTTPOnly属性是另一种Cookie属性,它可以防止JavaScript代码访问Cookie。JavaScript可以通过document.cookie API来访问Cookie,但是如果将Cookie设置为HTTPOnly,则它们将无法被JavaScript代码获取。这可以防止攻击者通过注入恶意脚本来窃取用户的Cookie,从而提高了Cookie的安全性。</li></ol><p>综合起来,secure和HTTPOnly属性的结合使用可以大大提高Cookie的安全性,使其更难以被攻击者利用。在设置Cookie时,建议使用这两种属性来确保Cookie的安全性,并且仅在需要将Cookie发送到服务器时才发送它们。需要注意的是,虽然使用secure和HTTPOnly属性可以帮助保护Cookie,但它们并不是完全安全的。攻击者仍然可以使用其他手段来窃取Cookie,例如使用钓鱼攻击来欺骗用户输入他们的凭据。因此,在处理敏感信息时,建议采取其他更全面的安全措施,例如使用多因素身份验证和数据加密等技术来确保数据安全。</p><h2 id="3、结尾"><a href="#3、结尾" class="headerlink" title="3、结尾"></a>3、结尾</h2><p>回到我们的问题,http协议secure为false,到底是不是漏洞?在阅读过第2章后,读者认真思考过应该有了答案。</p><hr><p>显而易见,肯定不是漏洞,HTTP协议根本就不需要secure!!!secure只用于https,在https登录的情况下,限制Cookie被http协议的请求传输。举个实际的例子</p><ul><li>http:</li></ul><p><img src="/img/image-20230305160119214.png" alt="image-20230305160119214"></p><ul><li>https:</li></ul><p><img src="/img/image-20230305160932809.png" alt="image-20230305160932809"></p>]]></content>
<tags>
<tag>http</tag>
</tags>
</entry>
</search>