├── 01_out_forward_heartbeat.md
├── 02_out_forward_buffered.md
├── 03_in_forward_plugin.md
├── 04_fluent_agent_lite.md
├── 05_coolio_cextension.md
├── README.md
└── toc.rb
/01_out_forward_heartbeat.md:
--------------------------------------------------------------------------------
1 | # Fluentd の out_forward と heartbeat
2 |
3 | フォワードを制するものは Fluentd を制す
4 |
5 | ## out_forward.rb の構造
6 |
7 | ### サンプル設定
8 |
9 | ```apache
10 |
11 | type forward
12 | # try_flush_interval 1
13 | flush_interval 0s # 60s
14 | buffer_queue_limit 312
15 | buffer_chunk_limit 10m
16 | num_threads 2
17 | retry_wait 1
18 | retry_limit 17
19 | max_retry_wait 4 # 131072
20 | # send_timeout 60s
21 | heartbeat_type udp
22 | heartbeat_interval 10s # 1s
23 | phi_threshold 65535 # 35 # 8
24 | recover_wait 50s # 10s
25 | hard_timeout 23s # 60s
26 |
27 | host host3
28 | port 12000
29 | weight 60
30 |
31 |
32 | host host2
33 | port 12001
34 | weight 60
35 |
36 |
37 | host host1
38 | port 13000
39 | weight 60
40 | standby true
41 |
42 |
43 | ```
44 |
45 | ついでの in_forward サンプル
46 |
47 | ```apache
48 |
49 | type forward
50 | port 12000
51 |
52 | ```
53 | ### パラメータたち
54 |
55 | まずドキュメントを見よう => [out_forward](http://docs.fluentd.org/ja/articles/out_forward)
56 |
57 | [out_forward.rb#L30-L50](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/plugin/out_forward.rb#L30-L50)
58 | ```ruby
59 | class ForwardOutput < ObjectBufferedOutput
60 | config_param :send_timeout, :time, :default => 60
61 | config_param :heartbeat_type, :default => :udp
62 | config_param :heartbeat_interval, :time, :default => 1
63 | config_param :recover_wait, :time, :default => 10
64 | config_param :hard_timeout, :time, :default => 60
65 | config_param :expire_dns_cache, :time, :default => nil # 0 means disable cache
66 | config_param :phi_threshold, :integer, :default => 16
67 | # backward compatibility
68 | config_param :port, :integer, :default => DEFAULT_LISTEN_PORT
69 | config_param :host, :string, :default => nil
70 | # directive はこちら
71 | https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/plugin/out_forward.rb#L67-L88
72 | end
73 | ```
74 |
75 | [output.rb#L162](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/output.rb#L162) こっちは次回
76 |
77 | ```ruby
78 | class BufferedOutput < Output
79 | config_param :buffer_type, :string, :default => 'memory'
80 | config_param :flush_interval, :time, :default => 60
81 | config_param :try_flush_interval, :float, :default => 1
82 | config_param :retry_limit, :integer, :default => 17
83 | config_param :retry_wait, :time, :default => 1.0
84 | config_param :max_retry_wait, :time, :default => nil
85 | config_param :num_threads, :integer, :default => 1
86 | config_param :queued_chunk_flush_interval, :time, :default => 1
87 | end
88 | ```
89 |
90 | ### クラスたち
91 |
92 | ```ruby
93 | class ForwardOutput < ObjectBufferedOutput
94 | # heartbeat を @heartbeat_interval ごとに打つ Timer スレッド
95 | class HeartbeatRequestTimer < Coolio::TimerWatcher
96 | # heartbeat を受け取る. event driven で io 多重化しているらしいが、Coolio 詳しくない
97 | class HeartbeatHandler < Coolio::IO
98 | # ディレクティブで設定したノードを表現するクラス
99 | class Node
100 | # heartbeat の結果を元に接続が失敗しているかを判定するクラス。端的には phi の計算
101 | class FailureDetector
102 | end
103 | ```
104 |
105 | ### メソッドたち
106 |
107 | ```ruby
108 | class ForwardOutput < ObjectBufferedOutput
109 | def initialize # new
110 | def configure(conf) # 設定. start より先に呼ばれる
111 | def start # engine の run ループ先頭で呼ばれる
112 | def shutdown # engine の run ループ終了で呼ばれる
113 | def run # engine の run ループで呼ばれる
114 | def write_objects # ObjectBufferedOutput のスレッドでデータを送るときに呼ばれる。node を選択してデータを投げる
115 | private
116 | def rebuild_weight_array # weight と heartbeat 結果を元に weight_array を作る
117 | def send_heartbeat_tcp # heartbeat_type :tcp 用 heartbeat
118 | def send_data # データをなげる
119 | def connect # 接続
120 |
121 | # heartbeat を @heartbeat_interval ごとにうつ Timer スレッド
122 | class HeartbeatRequestTimer < Coolio::TimerWatcher
123 | def initailize
124 | def on_timer
125 | end
126 | def on_timer # 時間がきたら heartbeat を投げる
127 |
128 | # heartbeat を受け取る. event driven で io 多重化しているらしいが、Coolio 詳しくない
129 | class HeartbeatHandler < Coolio::IO
130 | def initialize
131 | def on_readable # upd heartbeat の返答を受け取る
132 | end
133 | def on_heartbeat(sockaddr, msg) # heartbeat を元に rebuild_weight_array
134 |
135 | # ディレクティブで設定したノードを表現するクラス
136 | class Node
137 | def available?
138 | def standby?
139 | def resolved_host
140 | def resolve_dns!
141 | def tick # on_timer から呼ばれる。hard_timeout や phi_threshold の判定
142 | def heartbeat(detect=true) # heartbeat が成功したら呼ばれる
143 | def to_msgpack(out = '')
144 | end
145 |
146 | # heartbeat の結果を元に接続が失敗しているかを判定するクラス。端的には phi の計算
147 | class FailureDetector
148 | def initialize(heartbeat_interval, hard_timeout, init_last)
149 | def hard_timeout?(now) # hard_timeout 判定
150 | def add(now) # 成功時に add される
151 | def phi(now) # phi を計算する
152 | def sample_size
153 | def clear
154 | end
155 | end
156 | ```
157 |
158 | ## heartbeat の流れ
159 |
160 | ToDo: in_forward も絡めたシーケンス図をここに貼る
161 |
162 |
163 | [out_forward.rb#L108](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/plugin/out_forward.rb#L108) at start
164 |
165 | heatbeat_interval 1s ごとに起動するコールバックメソッド on_timer を登録.
166 |
167 | ```ruby
168 | if @heartbeat_type == :udp
169 | # assuming all hosts use udp
170 | @usock = SocketUtil.create_udp_socket(@nodes.first.host)
171 | @usock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
172 | @hb = HeartbeatHandler.new(@usock, method(:on_heartbeat))
173 | @loop.attach(@hb)
174 | end
175 |
176 | @timer = HeartbeatRequestTimer.new(@heartbeat_interval, method(:on_timer))
177 | @loop.attach(@timer)
178 | ```
179 |
180 | "\0" を全 node になげる. tcp の場合は connect(2) してすぐ close(2) [out_forward.rb#L205-L218](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/plugin/out_forward.rb#L205-L218)
181 |
182 | ```ruby
183 | def on_timer
184 | return if @finished
185 | @nodes.each {|n|
186 | if n.tick
187 | rebuild_weight_array
188 | end
189 | begin
190 | #log.trace "sending heartbeat #{n.host}:#{n.port} on #{@heartbeat_type}"
191 | if @heartbeat_type == :tcp
192 | send_heartbeat_tcp(n)
193 | else
194 | @usock.send "\0", 0, Socket.pack_sockaddr_in(n.port, n.resolved_host)
195 | end
196 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::EINTR
197 | # TODO log
198 | log.debug "failed to send heartbeat packet to #{n.host}:#{n.port}", :error=>$!.to_s
199 | end
200 | }
201 | end
202 | ```
203 |
204 | Node#tick。true を返す => heartbeat 失敗の意。イミフ
205 |
206 | ```ruby
207 | def tick
208 | now = Time.now.to_f
209 | if !@available
210 | if @failure.hard_timeout?(now)
211 | @failure.clear
212 | end
213 | return nil
214 | end
215 |
216 | if @failure.hard_timeout?(now)
217 | @log.warn "detached forwarding server '#{@name}'", :host=>@host, :port=>@port, :hard_timeout=>true
218 | @available = false
219 | @resolved_host = nil # expire cached host
220 | @failure.clear
221 | return true
222 | end
223 |
224 | phi = @failure.phi(now)
225 | #$log.trace "phi '#{@name}'", :host=>@host, :port=>@port, :phi=>phi
226 | if phi > @phi_threshold
227 | @log.warn "detached forwarding server '#{@name}'", :host=>@host, :port=>@port, :phi=>phi
228 | @available = false
229 | @resolved_host = nil # expire cached host
230 | @failure.clear
231 | return true
232 | else
233 | return false
234 | end
235 | end
236 | ```
237 |
238 | rebuld_weight_array
239 |
240 | ```
241 |
242 | type forward
243 |
244 | host xxxx
245 | port xxxx
246 | weight 60
247 |
248 |
249 | host xxxx
250 | port xxxx
251 | weight 40
252 |
253 |
254 | ```
255 |
256 | weight と node が生きているかどうかの情報を元に weight_array を構築。
257 |
258 |
259 | ```ruby
260 | def rebuild_weight_array
261 | standby_nodes, regular_nodes = @nodes.partition {|n|
262 | n.standby?
263 | }
264 |
265 | lost_weight = 0
266 | regular_nodes.each {|n|
267 | unless n.available?
268 | lost_weight += n.weight
269 | end
270 | }
271 | log.debug "rebuilding weight array", :lost_weight=>lost_weight
272 |
273 | if lost_weight > 0
274 | standby_nodes.each {|n|
275 | if n.available?
276 | regular_nodes << n
277 | log.warn "using standby node #{n.host}:#{n.port}", :weight=>n.weight
278 | lost_weight -= n.weight
279 | break if lost_weight <= 0
280 | end
281 | }
282 | end
283 |
284 | weight_array = []
285 | gcd = regular_nodes.map {|n| n.weight }.inject(0) {|r,w| r.gcd(w) }
286 | regular_nodes.each {|n|
287 | (n.weight / gcd).times {
288 | weight_array << n
289 | }
290 | }
291 |
292 | # for load balancing during detecting crashed servers
293 | coe = (regular_nodes.size * 6) / weight_array.size
294 | weight_array *= coe if coe > 1
295 |
296 | r = Random.new(@rand_seed)
297 | weight_array.sort_by! { r.rand }
298 |
299 | @weight_array = weight_array
300 | end
301 | ```
302 |
303 | udp heartbeat を送ると、in_forward で受け取って、udp で返す
304 |
305 | [in_forward.rb#L205-L232](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/plugin/in_forward.rb#L205-L232)
306 |
307 | ```ruby
308 | class HeartbeatRequestHandler < Coolio::IO
309 | def initialize(io, callback)
310 | super(io)
311 | @io = io
312 | @callback = callback
313 | end
314 |
315 | def on_readable
316 | begin
317 | msg, addr = @io.recvfrom(1024)
318 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::EINTR
319 | return
320 | end
321 | host = addr[3]
322 | port = addr[1]
323 | @callback.call(host, port, msg)
324 | rescue
325 | # TODO log?
326 | end
327 | end
328 |
329 | def on_heartbeat_request(host, port, msg)
330 | #log.trace "heartbeat request from #{host}:#{port}"
331 | begin
332 | @usock.send "\0", 0, host, port
333 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::EINTR
334 | end
335 | end
336 | ```
337 |
338 | upd heartbeat が帰ってきたら、rebuild_weight_array する
339 |
340 | [out_forward.rb#L295-L325](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/plugin/out_forward.rb#L295-L325)
341 |
342 | ```ruby
343 | class HeartbeatHandler < Coolio::IO
344 | def initialize(io, callback)
345 | super(io)
346 | @io = io
347 | @callback = callback
348 | end
349 |
350 | def on_readable
351 | begin
352 | msg, addr = @io.recvfrom(1024)
353 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::EINTR
354 | return
355 | end
356 | host = addr[3]
357 | port = addr[1]
358 | sockaddr = Socket.pack_sockaddr_in(port, host)
359 | @callback.call(sockaddr, msg)
360 | rescue
361 | # TODO log?
362 | end
363 | end
364 |
365 | def on_heartbeat(sockaddr, msg)
366 | port, host = Socket.unpack_sockaddr_in(sockaddr)
367 | if node = @nodes.find {|n| n.sockaddr == sockaddr }
368 | #log.trace "heartbeat from '#{node.name}'", :host=>node.host, :port=>node.port
369 | if node.heartbeat
370 | rebuild_weight_array
371 | end
372 | end
373 | end
374 | ```
375 |
376 | Node#heartbeat.[out_forward.rb#L295-L325](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/plugin/out_forward.rb#L295-L325)
377 |
378 | 引数は見つかった場合 true なのかというと、そういうことはなくて、
379 | heartbeat で反応があった場合 true で、send_data が成功した場合 false。というだけ。
380 | `@available` じゃない、かつ `@failure.sample_size > @recover_sample_size` の場合、recover させる。
381 |
382 |
383 | ```ruby
384 | def heartbeat(detect=true)
385 | now = Time.now.to_f
386 | @failure.add(now)
387 | #@log.trace "heartbeat from '#{@name}'", :host=>@host, :port=>@port, :available=>@available, :sample_size=>@failure.sample_size
388 | if detect && !@available && @failure.sample_size > @recover_sample_size
389 | @available = true
390 | @log.warn "recovered forwarding server '#{@name}'", :host=>@host, :port=>@port
391 | return true
392 | else
393 | return nil
394 | end
395 | end
396 | ```
397 |
398 | `@failure.sample_size` は heartbeat 失敗した数かと思いきや、そんなことはなくて、`#heartbeat` が呼ばれた回数。どちらかというと heartbeat 成功の数。
399 | deteched forwarding されると 0 にクリアされる。
400 |
401 | recover_sample_size は #configure の中で計算されていて、`@recover_wait` はデフォルト 10s で、`@heartbeat_interval` はデフォルト 1s。
402 | `recover_sample_size = @recover_wait / @heartbeat_interval`
403 |
404 | つまり、`@failure.sample_size > @recover_sample_size` は detached forwarding されてから、
405 | 何回 heartbeat 成功すれば recoverd forwarding 判定するか、を定義している。
406 | あくまで heartbeat 成功であって、recovered forwarding したノードにデータを送っていることを意味してはいない。
407 | `heartbeat_interval` を変えた場合は、`recover_wait` も変更すべきであろう。
408 |
409 |
410 | Failure Detector. [out_forward.rb#L435-L496](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/plugin/out_forward.rb#L435-L496)
411 |
412 | ```ruby
413 | class FailureDetector
414 | PHI_FACTOR = 1.0 / Math.log(10.0)
415 | SAMPLE_SIZE = 1000
416 |
417 | def initialize(heartbeat_interval, hard_timeout, init_last)
418 | @heartbeat_interval = heartbeat_interval
419 | @last = init_last
420 | @hard_timeout = hard_timeout
421 |
422 | # microsec
423 | @init_gap = (heartbeat_interval * 1e6).to_i
424 | @window = [@init_gap]
425 | end
426 |
427 | def hard_timeout?(now)
428 | now - @last > @hard_timeout
429 | end
430 |
431 | def add(now)
432 | if @window.empty?
433 | @window << @init_gap
434 | @last = now
435 | else
436 | gap = now - @last
437 | @window << (gap * 1e6).to_i
438 | @window.shift if @window.length > SAMPLE_SIZE
439 | @last = now
440 | end
441 | end
442 |
443 | def phi(now)
444 | size = @window.size
445 | return 0.0 if size == 0
446 |
447 | # Calculate weighted moving average
448 | mean_usec = 0
449 | fact = 0
450 | @window.each_with_index {|gap,i|
451 | mean_usec += gap * (1+i)
452 | fact += (1+i)
453 | }
454 | mean_usec = mean_usec / fact
455 |
456 | # Normalize arrive intervals into 1sec
457 | mean = (mean_usec.to_f / 1e6) - @heartbeat_interval + 1
458 |
459 | # Calculate phi of the phi accrual failure detector
460 | t = now - @last - @heartbeat_interval + 1
461 | phi = PHI_FACTOR * t / mean
462 |
463 | return phi
464 | end
465 |
466 | def sample_size
467 | @window.size
468 | end
469 |
470 | def clear
471 | @window.clear
472 | @last = 0
473 | end
474 | end
475 | ```
476 |
477 | まとめというか補足
478 |
479 | * デフォルトでは毎秒 udp 接続してパケットを投げる
480 | * in_forward は event driven 的に受け取ったらパケットを返して来る
481 | * packet がかえってくるまでの時間で phi がきまる
482 | * hard_timeout の時間1つもかえってこなかったらあきらめる
483 | * もっと早くあきらめるためのアルゴリズムとして phi_threshold がある
484 |
485 | * 毎秒パケットなげるのは量が多すぎじゃないのか。10s にしただけでネットワークだけでなく CPU 使用量も激減
486 | * phi の取り扱いが難しいので、hard_timeout だけでコントロールしている
487 | * event driven 的に I/O 多重化されているので accept までは1プロセスで複数やってくれるが、プラグインの処理が blocking 処理なので、accept した時点でブロックする。
488 | * heartbeat が受け取れなくなる
489 | * udp heartbeat なのでそんなに信頼性は高くない. heartbeat がロストする
490 |
--------------------------------------------------------------------------------
/02_out_forward_buffered.md:
--------------------------------------------------------------------------------
1 | # Fluentd の out_forward と BufferedOutput
2 |
3 | out_forward をサンプルにした BufferedOutput の解説
4 |
5 | * [out_forward のパラメータたち](#out_forward-%E3%81%AE%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%9F%E3%81%A1)
6 | * [out_foward の構造](#out_foward-%E3%81%AE%E6%A7%8B%E9%80%A0)
7 | * [ファイル構造](#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E6%A7%8B%E9%80%A0)
8 | * [クラス構造](#%E3%82%AF%E3%83%A9%E3%82%B9%E6%A7%8B%E9%80%A0)
9 | * [BufferedOutput のメソッド](#bufferedoutput-%E3%81%AE%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)
10 | * [BasicBuffer のメソッド](#basicbuffer-%E3%81%AE%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)
11 | * [スレッド状態](#%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E7%8A%B6%E6%85%8B)
12 | * [データ送信の流れ](#%E3%83%87%E3%83%BC%E3%82%BF%E9%80%81%E4%BF%A1%E3%81%AE%E6%B5%81%E3%82%8C)
13 | * [ObjectBufferedOutput#emit の流れ](#objectbufferedoutputemit-%E3%81%AE%E6%B5%81%E3%82%8C)
14 | * [ObjectBufferedOutput#try_flush の流れ(別スレッド)](#objectbufferedoutputtry_flush-%E3%81%AE%E6%B5%81%E3%82%8C%E5%88%A5%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89)
15 | * [ForwardOutput#write_objects の流れ](#forwardoutputwrite_objects-%E3%81%AE%E6%B5%81%E3%82%8C)
16 | * [shutdown 時の流れ](#shutdown-%E6%99%82%E3%81%AE%E6%B5%81%E3%82%8C)
17 | * [おまけ: USR1 を受け取った時の flush の流れ](#%E3%81%8A%E3%81%BE%E3%81%91-usr1-%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%81%A3%E3%81%9F%E6%99%82%E3%81%AE-flush-%E5%87%A6%E7%90%86%E3%81%AE%E6%B5%81%E3%82%8C)
18 | * [まとめおよび補足](#%E3%81%BE%E3%81%A8%E3%82%81%E3%81%8A%E3%82%88%E3%81%B3%E8%A3%9C%E8%B6%B3)
19 |
20 |
21 | ## out_forward のパラメータたち
22 |
23 | ドキュメントを見よう => [out_forward](http://docs.fluentd.org/ja/articles/out_forward)
24 |
25 | [out_forward.rb#L30-L50](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/plugin/out_forward.rb#L30-L50) この辺は前回カバーした
26 |
27 | ```ruby
28 | class ForwardOutput < ObjectBufferedOutput
29 | config_param :send_timeout, :time, :default => 60
30 | config_param :heartbeat_type, :default => :udp
31 | config_param :heartbeat_interval, :time, :default => 1
32 | config_param :recover_wait, :time, :default => 10
33 | config_param :hard_timeout, :time, :default => 60
34 | config_param :expire_dns_cache, :time, :default => nil # 0 means disable cache
35 | config_param :phi_threshold, :integer, :default => 16
36 | # backward compatibility
37 | config_param :port, :integer, :default => DEFAULT_LISTEN_PORT
38 | config_param :host, :string, :default => nil
39 | # directive はこちら
40 | https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/plugin/out_forward.rb#L67-L88
41 | end
42 | ```
43 |
44 | [output.rb#L162](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/output.rb#L162) 今回はこっち
45 |
46 | try_flush_interval と queued_chunk_flush_interval は実はドキュメントに載っていない
47 |
48 | ```ruby
49 | class BufferedOutput < Output
50 | config_param :buffer_type, :string, :default => 'memory'
51 | config_param :flush_interval, :time, :default => 60
52 | config_param :try_flush_interval, :float, :default => 1
53 | config_param :retry_limit, :integer, :default => 17
54 | config_param :retry_wait, :time, :default => 1.0
55 | config_param :max_retry_wait, :time, :default => nil
56 | config_param :num_threads, :integer, :default => 1
57 | config_param :queued_chunk_flush_interval, :time, :default => 1
58 | end
59 | ```
60 |
61 | [buffer.rb#L116-L132](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/buffer.rb#L116-L132)
62 |
63 | ```ruby
64 | class BasicBuffer < Buffer
65 | # This configuration assumes plugins to send records to a remote server.
66 | # Local file based plugins which should provide more reliability and efficiency
67 | # should override buffer_chunk_limit with a larger size.
68 | config_param :buffer_chunk_limit, :size, :default => 8*1024*1024
69 | config_param :buffer_queue_limit, :integer, :default => 256
70 | end
71 | ```
72 |
73 | ## out_foward の構造
74 |
75 | ### ファイル構造
76 |
77 | * [lib/fluent/plugin/out_forward.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/out_forward.rb) out_foward 本体
78 | * [lib/fluent/output.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb) Output プラグインの基底クラス。BufferedOutput など
79 | * [lib/fluent/plugin/buf_memory.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/buf_memory.rb) メモリ Buffer プラグイン
80 | * [lib/fluent/plugin/buf_file.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/buf_file.rb) ファイル Buffer プラグイン
81 | * [lib/fluent/buffer.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/buffer.rb) Buffer プラグインの基底クラス
82 |
83 |
84 | ### クラス構造
85 |
86 | ToDo: クラス図書く
87 |
88 | [lib/fluent/plugin/out_forward.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/out_forward.rb)
89 |
90 | ```ruby
91 | class ForwardOutput < ObjectBufferedOutput
92 | ```
93 |
94 | [lib/fluent/output.rb#L417-L451](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L417-L451)
95 |
96 | ObjectBufferedOutput は msgpack でメッセージのバッファリングを取り扱うクラス。
97 | 親の BufferedOutput 自体は json やその他のシリアライズも使えるように設計されている。
98 | [Writing Buffered Output Plugins](http://docs.fluentd.org/articles/plugin-development#writing-buffered-output-plugins) 参照.
99 |
100 | ```ruby
101 | class ObjectBufferedOutput < BufferedOutput
102 | ```
103 |
104 | BufferedOutput を継承したクラスには他にも
105 | [TimeSlicedOutput](http://docs.fluentd.org/articles/plugin-development#writing-time-sliced-output-plugins)
106 | がある。こちらは queue 数や chunk サイズでのバッファコントロールに加えて、
107 | 時間ごとにバッファリングする機能を拡張したクラス。
108 | out_file プラグインなどが利用している。今回は範囲外なので飛ばす。
109 |
110 | [lib/fluent/output.rb#L162-L414](https://github.com/fluent/fluentd/blob/443650b6b275e7e9f3a5afca2315019556439c38/lib/fluent/output.rb#L162-L414)
111 |
112 | BufferedOutput は Output クラスを継承しているだけでなく、バッファ Plugin を new して使っているのでそちらもカバーしていく。
113 |
114 | ```ruby
115 | class BufferedOutput < Output
116 | config_param :buffer_type, :string, :default => 'memory'
117 | ...
118 | ...
119 |
120 | def configure(conf)
121 | super
122 |
123 | @buffer = Plugin.new_buffer(@buffer_type)
124 | @buffer.configure(conf)
125 | end
126 | ```
127 |
128 | [lib/fluent/output.rb#L59-L159](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L59-L159)
129 |
130 | Output クラスは out_xxx プラグインを作るときに継承するクラス。実は config_param を生やすぐらいしかやってない。
131 |
132 | ```ruby
133 | class Output
134 | include Configurable # config_param メソッドを生やす
135 | include PluginId # conf[:id] を生やしているが、あんまり使っているの見た事ない
136 | include PluginLoggerMixin # v0.10.43 から導入された log メソッドを生やす
137 | ```
138 |
139 | [lib/fluent/plugin/buf_memory.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/buf_memory.rb)
140 |
141 | buffer_type memory とすると使われる。
142 |
143 | ```ruby
144 | class MemoryBufferChunk < BufferChunk
145 | class MemoryBuffer < BasicBuffer
146 | ```
147 |
148 | [lib/fluent/plugin/buf_file.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/buf_file.rb)
149 |
150 | buffer_type file とすると使われる。今回は飛ばす。
151 |
152 | ```ruby
153 | class FileBufferChunk < BufferChunk
154 | class FileBuffer < BasicBuffer
155 | ```
156 |
157 | [lib/fluent/buffer.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/buffer.rb)
158 |
159 | BasicBuffer は Buffer プラグインの基底クラス。buffer_chunk_limit, buffer_queue_limit のオプションは BasicBuffer クラスで設定されている。
160 |
161 | 独自に Buffer プラグインを作って、buffer_type に独自プラグインを指定させることもできる。
162 | ということになっているが、作ってる人みたことない。=> あ、[fluent-plugin-buffer-lightening](https://github.com/tagomoris/fluent-plugin-buffer-lightening) ありましたね。
163 |
164 | ```ruby
165 | class Buffer
166 | class BufferChunk
167 | include MonitorMixin
168 | class BasicBuffer < Buffer
169 | include MonitorMixin
170 | ```
171 |
172 | ちなみに [MonitorMixin](http://docs.ruby-lang.org/ja/2.0.0/class/MonitorMixin.html) は ruby で提供されているスレッドのモニター機能を提供するモジュール。
173 | [ConditionVariable](http://docs.ruby-lang.org/ja/2.0.0/class/MonitorMixin=3a=3aConditionVariable.html) 参照。
174 |
175 |
176 | ## BufferedOutput のメソッド
177 |
178 | BufferedOutput [output.rb#L162-L414](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L162-L414)
179 |
180 | ```ruby
181 | class BufferedOutput < Output
182 | def initialize # new
183 | def configure(conf) # オプション処理
184 | def start # スレッドスタート
185 | def shutdown # スレッドストップ
186 | def emit(tag, es, chain, key="") # Fluentd本体からデータを送られるエンドポイント
187 | def submit_flush #すぐ try_flush が呼ばれるように時間フラグを0にリセット
188 | def format_stream(tag, es) # データ stream のシリアライズ
189 | #def format(tag, time, record) # format_stream が呼び出す。単データのシリアライズをここで定義する
190 | #def write(chunk) # Buffer スレッドがこのメソッドを呼び出す
191 | def enqueue_buffer # Buffer キューにデータを追加
192 | def try_flush # Buffer キューからデータを取り出して flush する。flush 処理は別スレッドで実行される。@buffer.pop を呼び出す。
193 | def force_flush # USR1 シグナルを受けた時に強制 flush する
194 | def before_shutdown # shutdown 前処理
195 | def calc_retry_wait # retry 時間間隔の計算(exponential backoff)
196 | def write_abort # retry 限界を超えた場合に buffer を捨てる
197 | def flush_secondary(secondary) # secondary ノードへの flush
198 | end
199 | ```
200 |
201 | OutputThread [output.rb#L89-L159](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L89-L159)
202 |
203 | ```ruby
204 | # num_threads で output プラグインの並列数を増やすために利用
205 | class OutputThread
206 | def initialize(output) # output プラグイン自身を渡す
207 | def configure(conf)
208 | def start # スレッドスタート
209 | def shutdown # スレッド終了
210 | def submit_flush #すぐ try_flush が呼ばれるように時間フラグを0にリセット
211 | private
212 | def run # スレッドループ。時間がきたら try_flush を呼ぶ
213 | def cond_wait(sec) # ConditionVariable を使って sec 秒待つ
214 | end
215 | ```
216 |
217 | ObjectBufferedOutput [output.rb#L417-L451](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L417-L451)
218 |
219 | ```ruby
220 | class ObjectBufferedOutput < BufferedOutput
221 | def initialize
222 | def emit(tag, es, chain) # msgpack にシリアライズするようにオーバーライド
223 | def write(chunk) # msgpack にシリアライズするようにオーバーライド. write_objects(chunk.key, chunk) を呼び出す
224 | end
225 | ```
226 |
227 | ForwardOutput
228 |
229 | [out_forward.rb#L129](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/out_forward.rb#L129)
230 |
231 | ```ruby
232 | class FowardOutput < ObjectBufferedOutput
233 | def write_objects(tag, chunk) # データ送信する
234 | end
235 | ```
236 |
237 | ## BasicBuffer のメソッド
238 |
239 | BasicBuffer [buffer.rb#L116-L305](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/buffer.rb#L116-L305)
240 |
241 | ```ruby
242 | class BasicBuffer < Buffer
243 | def initialize
244 | def enable_parallel(b=true)
245 | config_param :buffer_chunk_limit, :size, :default => 8*1024*1024
246 | config_param :buffer_queue_limit, :integer, :default => 256
247 | def configure(conf) # プラグインオプション設定
248 | def start # buffer plugin の start
249 | def shutdown # buffer plugin の shutdown
250 | def storable?(chunk, data) # buffer_chunk_limit を超えていないかどうか
251 | def emit(key, data, chain) # Fluentd本体からデータを送られるエンドポイント
252 | def keys # 取り扱っている key (基本的には tag) 一覧
253 | def queue_size # queue のサイズ
254 | def total_queued_chunk_size # キューに入っている全 chunk のサイズ
255 | #def new_chunk(key) # 扱う chunk オブジェクトの定義をすること
256 | #def resume # queue, map の初期化定義をすること
257 | #def enqueue(chunk) # chunk の enqueue を定義すること
258 | def push(key) # BufferedOutput が try_flush する時に呼び出されるようだ
259 | def pop(out) # queue から chunk を取り出して write_chunk する
260 | def write_chunk(chunk, out) # 中身は out.write(chunk)
261 | def clear! # queue をクリアする
262 | end
263 | ```
264 |
265 | MemoryBuffer [buf_memory.rb#L67-L102](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/buf_memory.rb#L67-L102)
266 |
267 | ```ruby
268 | class MemoryBuffer < BasicBuffer
269 | def initialize
270 | def configure(conf)
271 | def before_shutdown(out)
272 | def new_chunk(key) # MemoryBufferChunk.new(key)
273 | def resume # @queue, @map = [], {}
274 | def enqueue(chunk) # 空
275 | end
276 | ```
277 |
278 | ## スレッド状態
279 |
280 | メインスレッドと num_threads 数の OutputThread スレッド
281 |
282 | ```ruby
283 | class BufferedOutput < Output
284 | ...
285 | config_param :buffer_type, :string, :default => 'memory'
286 | ...
287 | config_param :num_threads, :integer, :default => 1
288 |
289 | def configure(conf)
290 | super
291 |
292 | @buffer = Plugin.new_buffer(@buffer_type)
293 | @buffer.configure(conf)
294 | ...
295 | @writers = (1..@num_threads).map {
296 | writer = OutputThread.new(self)
297 | writer.configure(conf)
298 | writer
299 | }
300 | ...
301 | end
302 |
303 | def start
304 | ...
305 | @writers.each {|writer| writer.start }
306 | ...
307 | end
308 |
309 | def shutdown
310 | @writers.each {|writer| writer.shutdown }
311 | end
312 | ```
313 |
314 | OutputThread [output.rb#L89-L159](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L89-L159)
315 |
316 | run ループを別スレッドで回して、定期的に output#try_flush を呼ぶ。
317 | try_flush ではデータを queue から pop してデータ送信する。
318 |
319 | つまり、データ送信を複数の別スレッドで並列に行っている。
320 | 特に、対向 Fluentd プロセスが複数ある場合に有効。
321 | 1つの場合でも受信側が nonblocking でデータをうまく捌いてくれるような場合には有効になりえるだろう。
322 |
323 | ```ruby
324 | class OutputThread
325 | def initialize(output)
326 | @output = output
327 | @finish = false
328 | @next_time = Engine.now + 1.0
329 | end
330 |
331 | def configure(conf)
332 | end
333 |
334 | def start
335 | @mutex = Mutex.new
336 | @cond = ConditionVariable.new
337 | @thread = Thread.new(&method(:run))
338 | end
339 |
340 | def shutdown
341 | @finish = true
342 | @mutex.synchronize {
343 | @cond.signal
344 | }
345 | Thread.pass
346 | @thread.join
347 | end
348 |
349 | def submit_flush
350 | @mutex.synchronize {
351 | @next_time = 0
352 | @cond.signal
353 | }
354 | Thread.pass
355 | end
356 |
357 | private
358 | def run
359 | @mutex.lock
360 | begin
361 | until @finish
362 | time = Engine.now
363 |
364 | if @next_time <= time
365 | @mutex.unlock
366 | begin
367 | @next_time = @output.try_flush
368 | ensure
369 | @mutex.lock
370 | end
371 | next_wait = @next_time - Engine.now
372 | else
373 | next_wait = @next_time - time
374 | end
375 |
376 | cond_wait(next_wait) if next_wait > 0
377 | end
378 | ensure
379 | @mutex.unlock
380 | end
381 | rescue
382 | $log.error "error on output thread", :error=>$!.to_s
383 | $log.error_backtrace
384 | raise
385 | ensure
386 | @mutex.synchronize {
387 | @output.before_shutdown
388 | }
389 | end
390 |
391 | def cond_wait(sec)
392 | @cond.wait(@mutex, sec)
393 | end
394 | end
395 | ```
396 |
397 | #### 補足: ConditionVariable
398 |
399 | * [class ConditionVariable](http://docs.ruby-lang.org/ja/2.1.0/class/ConditionVariable.html)
400 | * [Rubyを用いたマルチスレッド対応Queueの作り方 - M-Tea](http://www.m-tea.info/2009/12/rubyqueue.html)
401 |
402 | あたりが参考になるかも
403 |
404 | ## データ送信の流れ
405 |
406 | シーケンス図 => あとで書くかもしれないが、今は [Fluentdがよくわからなかった話#29](http://www.slideshare.net/harukayon/fluentd-22317236#29) を見てもらえると。
407 |
408 | * メインスレッド(データ入力があった)
409 |
410 | * ObjectBufferedOutput#emit
411 |
412 | * BasicBuffer#emit
413 |
414 | * 別スレッド(時間がたった)
415 |
416 | * ObjectBufferedOutput#try_flush
417 |
418 | * BasicBuffer#push
419 | * BasicBuffer#pop
420 |
421 | ### ObjectBufferedOutput#emit の流れ
422 |
423 | Fluentd プラグインの仕組みが #emit を呼び出してデータを送って来る。通常は chunk の key は tag だが、TimeSlicedOutput の場合は時間を format した文字列になり、key ごとに chunk が作られる。
424 |
425 | [ObjectBufferedOutput#emit](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L417-L429)
426 |
427 | ```ruby
428 | class ObjectBufferedOutput < BufferedOutput
429 | ...
430 |
431 | def emit(tag, es, chain)
432 | @emit_count += 1
433 | data = es.to_msgpack_stream
434 | key = tag
435 | if @buffer.emit(key, data, chain)
436 | submit_flush
437 | end
438 | end
439 | ```
440 |
441 | [BasicBuffer#emit](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/buffer.rb#L165-L214) @buffer.emit
442 |
443 | * key (通常は tag) 毎に chunk を処理
444 | * top chunk が buffer_chunk_limit を超えてなければ chunk に data を格納
445 | * 超えていれば次の処理
446 | * 入らなかったデータを next chunk に格納
447 | * top chunk を queue に格納
448 | * 次の top chunk を next chunk で更新
449 |
450 | ```ruby
451 | def start
452 | @queue, @map = resume
453 | @queue.extend(MonitorMixin)
454 | end
455 |
456 | def emit(key, data, chain)
457 | key = key.to_s
458 |
459 | synchronize do
460 | top = (@map[key] ||= new_chunk(key)) # TODO generate unique chunk id
461 |
462 | # top chunk が buffer_chunk_limit を超えてなければ chunk に data を格納
463 | if storable?(top, data)
464 | chain.next
465 | top << data
466 | return false
467 |
468 | ## FIXME
469 | #elsif data.bytesize > @buffer_chunk_limit
470 | # # TODO
471 | # raise BufferChunkLimitError, "received data too large"
472 |
473 | elsif @queue.size >= @buffer_queue_limit
474 | raise BufferQueueLimitError, "queue size exceeds limit"
475 | end
476 |
477 | if data.bytesize > @buffer_chunk_limit
478 | $log.warn "Size of the emitted data exceeds buffer_chunk_limit."
479 | $log.warn "This may occur problems in the output plugins ``at this server.``"
480 | $log.warn "To avoid problems, set a smaller number to the buffer_chunk_limit"
481 | $log.warn "in the forward output ``at the log forwarding server.``"
482 | end
483 |
484 | nc = new_chunk(key) # TODO generate unique chunk id
485 | ok = false
486 |
487 | begin
488 | nc << data # 入らなかった分を next chunk に格納
489 | chain.next
490 |
491 | flush_trigger = false
492 | @queue.synchronize {
493 | enqueue(top)
494 | flush_trigger = @queue.empty?
495 | @queue << top # top chunk を queue に格納
496 | @map[key] = nc # 次の top chunk を更新
497 | }
498 |
499 | ok = true
500 | return flush_trigger
501 | ensure
502 | nc.purge unless ok
503 | end
504 |
505 | end # synchronize
506 | end
507 | ```
508 |
509 | [MemoryBufferChunk](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/buf_memory.rb#L19-L64) new_chunk の実体
510 |
511 | @buffer << data で、文字列として追記しているだけ
512 |
513 | ```ruby
514 | class MemoryBufferChunk < BufferChunk
515 | def initialize(key, data='')
516 | @data = data
517 | @data.force_encoding('ASCII-8BIT')
518 | now = Time.now.utc
519 | u1 = ((now.to_i*1000*1000+now.usec) << 12 | rand(0xfff))
520 | @unique_id = [u1 >> 32, u1 & u1 & 0xffffffff, rand(0xffffffff), rand(0xffffffff)].pack('NNNN')
521 | super(key)
522 | end
523 |
524 | attr_reader :unique_id
525 |
526 | def <<(data)
527 | data.force_encoding('ASCII-8BIT')
528 | @data << data # 文字列として追記しているだけ
529 | end
530 |
531 | def size
532 | @data.bytesize
533 | end
534 |
535 | def close
536 | end
537 |
538 | def purge
539 | end
540 |
541 | def read
542 | @data
543 | end
544 |
545 | def open(&block)
546 | StringIO.open(@data, &block)
547 | end
548 |
549 | # optimize
550 | def write_to(io)
551 | io.write @data
552 | end
553 |
554 | # optimize
555 | def msgpack_each(&block)
556 | u = MessagePack::Unpacker.new
557 | u.feed_each(@data, &block)
558 | end
559 | end
560 | ```
561 |
562 | [MemoryBuffer](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/buf_memory.rb#L67-L102) @queue の実体, enqueue(chunk) の定義
563 |
564 | メモリバッファの場合、@queue はただの配列で、@map もただのハッシュ。enqueue(chunk) でもとくに何もやっていない。
565 | [FileBuffer](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/buf_file.rb#L76) の場合は色々やっているが、今回はカバーしない。
566 |
567 | ```ruby
568 | class MemoryBuffer < BasicBuffer
569 | Plugin.register_buffer('memory', self)
570 |
571 | def initialize
572 | super
573 | end
574 |
575 | # Overwrite default BasicBuffer#buffer_queue_limit
576 | # to limit total memory usage upto 512MB.
577 | config_set_default :buffer_queue_limit, 64
578 |
579 | def configure(conf)
580 | super
581 | end
582 |
583 | def before_shutdown(out)
584 | synchronize do
585 | @map.each_key {|key|
586 | push(key)
587 | }
588 | while pop(out)
589 | end
590 | end
591 | end
592 |
593 | def new_chunk(key)
594 | MemoryBufferChunk.new(key)
595 | end
596 |
597 | def resume
598 | return [], {} # @queue が [] で @map が {}
599 | end
600 |
601 | def enqueue(chunk)
602 | # なにもやってない
603 | end
604 | end
605 | ```
606 |
607 | ### ObjectBufferedOutput#try_flush の流れ(別スレッド)
608 |
609 | try_flush は OutputThread の run ループから try_flush_interval 毎に呼び出される
610 | ※ try_flush_interval がデフォルトの 1 の場合、flush_interval 0s にしても 1 秒毎にしか flush されない。
611 |
612 |
613 | 前半のみ読む [output.rb#L271-L325](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L271-L325)
614 |
615 | 主要な流れをざっくり
616 |
617 | * buffer_chunk_limit に達していなくても flush_interval が来たら enqueue する
618 | * queue が空の場合のみ、enqueue 処理が走る
619 | * key (通常は tag) それぞれの chunk を一気に enqueue する
620 | * queue から chunk を 1つ pop して output#write (正確には、取り出して => write して => 成功したら削除)
621 |
622 | ```ruby
623 | def try_flush
624 | time = Engine.now
625 |
626 | empty = @buffer.queue_size == 0
627 | if empty && @next_flush_time < (now = Engine.now)
628 | @buffer.synchronize do
629 | if @next_flush_time < now
630 | enqueue_buffer # buffer_chunk_limit に達していなくても flush_interval が来たら enqueue する
631 | @next_flush_time = now + @flush_interval
632 | empty = @buffer.queue_size == 0
633 | end
634 | end
635 | end
636 | if empty
637 | return time + @try_flush_interval
638 | end
639 |
640 | begin
641 | retrying = !@error_history.empty?
642 |
643 | if retrying
644 | @error_history.synchronize do
645 | if retrying = !@error_history.empty? # re-check in synchronize
646 | if @next_retry_time >= time
647 | # allow retrying for only one thread
648 | return time + @try_flush_interval
649 | end
650 | # assume next retry failes and
651 | # clear them if when it succeeds
652 | @last_retry_time = time
653 | @error_history << time
654 | @next_retry_time += calc_retry_wait
655 | end
656 | end
657 | end
658 |
659 | if @secondary && @error_history.size > @retry_limit
660 | has_next = flush_secondary(@secondary)
661 | else
662 | has_next = @buffer.pop(self) # queue から chunk を pop して output#write
663 | end
664 |
665 | # success
666 | if retrying
667 | @error_history.clear
668 | # Note: don't notify to other threads to prevent
669 | # burst to recovered server
670 | $log.warn "retry succeeded.", :instance=>object_id
671 | end
672 |
673 | if has_next
674 | return Engine.now + @queued_chunk_flush_interval
675 | else
676 | return time + @try_flush_interval
677 | end
678 | ```
679 |
680 |
681 | [BufferedOutput#enqueue_buffer](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L265-L269)
682 |
683 | ```ruby
684 | def enqueue_buffer
685 | @buffer.keys.each {|key|
686 | @buffer.push(key)
687 | }
688 | end
689 | ```
690 |
691 | [BasicBuffer#push](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/buffer.rb#L244-L259)
692 |
693 | flush_interval が来たので、buffer_chunk_limit に達していないが、top chunk を @queue に積み、削除している。
694 | この enqueue (push) 処理は全 key (通常は tag) に対してまとめて行われる。
695 | chunk は key (通常は tag) 毎に作られるので、tag が異なる多様なログを受け取っている場合、その数だけ @queue に一気に積み上げられる。
696 |
697 | ```ruby
698 | def push(key)
699 | synchronize do
700 | top = @map[key]
701 | if !top || top.empty?
702 | return false
703 | end
704 |
705 | @queue.synchronize do
706 | enqueue(top)
707 | @queue << top
708 | @map.delete(key)
709 | end
710 |
711 | return true
712 | end # synchronize
713 | end
714 | ```
715 |
716 | [BasicBuffer#pop](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/buffer.rb#L261-L293)
717 |
718 | @queue から chunk を1つ取り出して、データ送信を行う。
719 |
720 | pop というメソッド名だが、pop するだけではなく、write (送信)もしている。write 成功した場合のみ、取り除く。
721 |
722 | ```ruby
723 | def pop(out)
724 | chunk = nil
725 | @queue.synchronize do
726 | if @parallel_pop
727 | chunk = @queue.find {|c| c.try_mon_enter }
728 | return false unless chunk
729 | else
730 | chunk = @queue.first
731 | return false unless chunk
732 | return false unless chunk.try_mon_enter
733 | end
734 | end
735 |
736 | begin
737 | if !chunk.empty?
738 | write_chunk(chunk, out) # out.write(chunk) を呼び出す
739 | end
740 |
741 | empty = false
742 | @queue.synchronize do
743 | @queue.delete_if {|c|
744 | c.object_id == chunk.object_id
745 | }
746 | empty = @queue.empty?
747 | end
748 |
749 | chunk.purge
750 |
751 | return !empty
752 | ensure
753 | chunk.mon_exit
754 | end
755 | end
756 | ```
757 |
758 | 大事な補足:enqueue 処理は flush\_interval 毎に1回される。全 key (通常は tag) の chunk がまとめて enqueue される。
759 | また pop (and 送信) 処理は try\_flush\_interval 毎に実行されるが、こちらは 1 chunk しか pop されない。
760 | buffer_chunk_limit が小さい場合、十分なパフォーマンスが出ない可能性があるし、 enqueue された chunk を連続して一斉に pop したい場合、queued_chunk_flush_interval をごくごく小さな値に設定する必要がある。
761 |
762 | 考察:try_flush_interval 毎の pop (and 送信)処理で、複数 chunk 一気に送るようにしたらどうだろう。
763 |
764 | ### ForwardOutput#write_objects の流れ
765 |
766 | BasicBuffer#try_flush
767 | => [BasicBuffer#write_chunk(chunk)](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/buffer.rb#L295-L297)
768 | => [BufferedOutput#write(chunk)](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L447-L450)
769 | => [ForwardOutput#write_objects](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/out_forward.rb#L129-L155)
770 |
771 | [rebuild_weight_array](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/out_forward.rb#L159-L199) して
772 | weight と heartbeat の結果を元にランダムに構築した @weight_array の順に[send_data](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/out_forward.rb#L220-L255) を試みる。
773 |
774 | ```ruby
775 | def write_objects(tag, chunk)
776 | return if chunk.empty?
777 |
778 | error = nil
779 |
780 | wlen = @weight_array.length
781 | wlen.times do
782 | @rr = (@rr + 1) % wlen
783 | node = @weight_array[@rr]
784 |
785 | if node.available?
786 | begin
787 | send_data(node, tag, chunk)
788 | return
789 | rescue
790 | # for load balancing during detecting crashed servers
791 | error = $! # use the latest error
792 | end
793 | end
794 | end
795 |
796 | if error
797 | raise error # 全てのノードで送信失敗した場合。例外メッセージとして見れるのは最後の例外のみ
798 | else
799 | raise "no nodes are available" # 全てのノードがそもそも available ではなかった場合
800 | end
801 | end
802 | ```
803 |
804 | [ForwardOutput#send_data](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/out_forward.rb#L220-L255)
805 |
806 | chunk を送るたびに socket を開いて、閉じている。SO_LINGER オプションで @send_timeout 時間を設定(時間がすぎたら RESET パケット)
807 |
808 | ```ruby
809 | def send_data(node, tag, chunk)
810 | sock = connect(node)
811 | begin
812 | opt = [1, @send_timeout.to_i].pack('I!I!') # { int l_onoff; int l_linger; }
813 | sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, opt)
814 |
815 | opt = [@send_timeout.to_i, 0].pack('L!L!') # struct timeval
816 | sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, opt)
817 |
818 | # beginArray(2)
819 | sock.write FORWARD_HEADER
820 |
821 | # writeRaw(tag)
822 | sock.write tag.to_msgpack # tag
823 |
824 | # beginRaw(size)
825 | sz = chunk.size
826 | #if sz < 32
827 | # # FixRaw
828 | # sock.write [0xa0 | sz].pack('C')
829 | #elsif sz < 65536
830 | # # raw 16
831 | # sock.write [0xda, sz].pack('Cn')
832 | #else
833 | # raw 32
834 | sock.write [0xdb, sz].pack('CN')
835 | #end
836 |
837 | # writeRawBody(packed_es)
838 | chunk.write_to(sock)
839 |
840 | node.heartbeat(false)
841 | ensure
842 | sock.close
843 | end
844 | end
845 | ```
846 |
847 | [BufferedOutput#try_flush 後半](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L389-L399)
848 |
849 | write_objects が raise した場合、try_flush の rescue 節に入る。
850 |
851 | ```ruby
852 | ...
853 | has_next = @buffer.pop(self)
854 | ...
855 | rescue => e
856 | if retrying
857 | error_count = @error_history.size
858 | else
859 | # first error
860 | error_count = 0
861 | @error_history.synchronize do
862 | if @error_history.empty?
863 | @last_retry_time = time
864 | @error_history << time
865 | @next_retry_time = time + calc_retry_wait
866 | end
867 | end
868 | end
869 |
870 | if error_count < @retry_limit
871 | $log.warn "temporarily failed to flush the buffer.", :next_retry=>Time.at(@next_retry_time), :error_class=>e.class.to_s, :error=>e.to_s, :instance=>object_id
872 | $log.warn_backtrace e.backtrace
873 |
874 | elsif @secondary
875 | if error_count == @retry_limit
876 | $log.warn "failed to flush the buffer.", :error_class=>e.class.to_s, :error=>e.to_s, :instance=>object_id
877 | $log.warn "retry count exceededs limit. falling back to secondary output."
878 | $log.warn_backtrace e.backtrace
879 | retry # retry immediately
880 | elsif error_count <= @retry_limit + @secondary_limit
881 | $log.warn "failed to flush the buffer, next retry will be with secondary output.", :next_retry=>Time.at(@next_retry_time), :error_class=>e.class.to_s, :error=>e.to_s, :instance=>object_id
882 | $log.warn_backtrace e.backtrace
883 | else
884 | $log.warn "failed to flush the buffer.", :error_class=>e.class, :error=>e.to_s, :instance=>object_id
885 | $log.warn "secondary retry count exceededs limit."
886 | $log.warn_backtrace e.backtrace
887 | write_abort
888 | @error_history.clear
889 | end
890 |
891 | else
892 | $log.warn "failed to flush the buffer.", :error_class=>e.class.to_s, :error=>e.to_s, :instance=>object_id
893 | $log.warn "retry count exceededs limit."
894 | $log.warn_backtrace e.backtrace
895 | write_abort
896 | @error_history.clear
897 | end
898 |
899 | return @next_retry_time
900 | end
901 | ```
902 |
903 | [BufferedOutput#calc_retry_wait](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L389-L399)
904 |
905 | calc_retry_wait で倍々で wait 時間をのばしている。いわゆる exponential backoff。
906 |
907 | ```ruby
908 | def calc_retry_wait
909 | # TODO retry pattern
910 | wait = if @error_history.size <= @retry_limit
911 | @retry_wait * (2 ** (@error_history.size-1))
912 | else
913 | # secondary retry
914 | @retry_wait * (2 ** (@error_history.size-2-@retry_limit))
915 | end
916 | retry_wait = wait + (rand * (wait / 4.0) - (wait / 8.0))
917 | @max_retry_wait ? [retry_wait, @max_retry_wait].min : retry_wait
918 | end
919 | ```
920 |
921 | ## shutdown 時の流れ
922 |
923 | BufferedOutput の [#run ループが終わって](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L152)、#before_shutdown が呼ばれる
924 |
925 | [lib/fluent/output.rb#L380-L387](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L380-L387)
926 |
927 | ```ruby
928 | def before_shutdown
929 | begin
930 | @buffer.before_shutdown(self)
931 | rescue
932 | $log.warn "before_shutdown failed", :error=>$!.to_s
933 | $log.warn_backtrace
934 | end
935 | end
936 | ```
937 |
938 | [lib/fluent/plugin/buf_memory.rb#L82-L90](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/buf_memory.rb#L82-L90)
939 |
940 | 全部吐き出そうと試みる。※ 吐き出さずにすぐ終了するオプション欲しいな => [pull requested](https://github.com/fluent/fluentd/pull/286)
941 |
942 | ```ruby
943 | def before_shutdown(out)
944 | synchronize do
945 | @map.each_key {|key|
946 | push(key)
947 | }
948 | while pop(out)
949 | end
950 | end
951 | end
952 | ```
953 |
954 | ## おまけ: USR1 を受け取った時の flush 処理の流れ
955 |
956 | USR1 シグナルを送ると、Buffer の内容を flush してくれることになっている。
957 |
958 | [lib/fluent/supervisor.rb#L367-L382](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/supervisor.rb#L367-L382)
959 |
960 | ```ruby
961 | trap :USR1 do
962 | $log.debug "fluentd main process get SIGUSR1"
963 | $log.info "force flushing buffered events"
964 | @log.reopen!
965 |
966 | # Creating new thread due to mutex can't lock
967 | # in main thread during trap context
968 | Thread.new {
969 | begin
970 | Fluent::Engine.flush!
971 | $log.debug "flushing thread: flushed"
972 | rescue Exception => e
973 | $log.warn "flushing thread error: #{e}"
974 | end
975 | }.run
976 | end
977 | ```
978 |
979 | [lib/fluent/engine.rb#L279-L295](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/engine.rb#L279-L295)
980 |
981 | ```ruby
982 | def flush!
983 | flush_recursive(@matches)
984 | end
985 |
986 | ...
987 |
988 | def flush_recursive(array)
989 | array.each {|m|
990 | begin
991 | if m.is_a?(Match)
992 | m = m.output
993 | end
994 | if m.is_a?(BufferedOutput)
995 | m.force_flush
996 | elsif m.is_a?(MultiOutput)
997 | flush_recursive(m.outputs)
998 | end
999 | rescue => e
1000 | $log.debug "error while force flushing", :error_class=>e.class, :error=>e
1001 | $log.debug_backtrace
1002 | end
1003 | }
1004 | end
1005 | ```
1006 |
1007 | [lib/fluent/output.rb#L375-L378](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/output.rb#L375-L378)
1008 |
1009 | ```ruby
1010 | def force_flush
1011 | enqueue_buffer
1012 | submit_flush
1013 | end
1014 | ```
1015 |
1016 |
1017 |
1018 | ## まとめおよび補足
1019 |
1020 | * BufferedOutput, BasicBuffer 周りの処理を読んだ
1021 | * buffer_chunk_limit と buffer_queue_limit
1022 | * enqueue のタイミング2つ
1023 | * メインスレッド(ObjectBufferedOutput#emit) で、chunk にデータを追加すると buffer_chunk_limit を超える場合
1024 | * OutputThread (ObjectBufferedOutput#try_flush) で、flush_interval 毎
1025 | * queue が空の場合のみ、enqueue 処理が走る
1026 | * key (通常は tag) それぞれの chunk を一気に enqueue する
1027 | * dequeue(pop) のタイミング
1028 | * queue に次の chunk がある場合、queued_chunk_flush_interval 毎
1029 | * queue に次の chunk がない場合、try_flush_interval 毎
1030 | * このタイミングで 1 chunk しか output#write されないので、パフォーマンスをあげるには chunk サイズを増やすか、queued_chunk_flush_interval および try_flush_interval を短くする必要がある。
1031 | * num_threads を増やすと OutputThread の数が増えるので output#write の IO 処理が並列化されて性能向上できる可能性
1032 | * ForwardOutput#send_data はデータを送るたびに connect(2) して close(2) しているので keepalive することで性能向上できる可能性
1033 |
1034 | 性能評価結果からの補足
1035 |
1036 | * パフォーマンスをあげるためには buffer_chunk_limit を増やすと良い、と言ったが実際に buffer_chunk_limit を増やすと 8m ぐらいで詰まりやすくなり、性能劣化する。[out_forward って詰まると性能劣化する?](http://togetter.com/li/595607)
1037 | * なので、buffer_chunk_limit は 1m ぐらいに保ちつつ try_flush_interval を 0.1、queued_chunk_flush_interval を 0.01 など小さい値にしてじゃんじゃん吐き出すと良さそう
1038 |
1039 | 考察
1040 |
1041 | * もしくは chunk のない out_foward (BufferedOutput)拡張を作って1行ごとに吐き出せばじゃんじゃん送信できそう。その場合 keepalive 必須にしないとパフォーマンス出ないと思う。
1042 | * でも小さすぎると keepalive とはいえ、ネットワーク的に効率がよくない
1043 | * もしくは1度の dequeue(pop) のタイミングで、1 chunk ではなく 10 chunk まとめて pop できるようなオプションを付ければ chunk サイズを小さく保ちつつパフォーマンスをあげることができそう
1044 | * [Fluentd の BufferedOutput に、1度に複数の chunk を吐き出すオプションを足すことについて議論](http://togetter.com/li/651190)
1045 | * 結論としては、queued_chunk_flush_interval を 0.001 のように小さくして、try_flush_interval は 1 のまま、とかでよさそう。
1046 | * むしろ、queue もいらないのでは。in_tail => out_forward において、out_forward が送信失敗した場合は、ログを読み込まなければよい。fluent-agent-lite には queue はない。
1047 | * in_tail と out_forward をがっちゃんこした in_tail_forward プラグインとかを作ればできるかも。
1048 | * buffer_queue_limit を超えた場合、例外を発生させて、in プラグインにまで伝搬させれば、がっしゃんこプラグインを作られなくても扱えれるんじゃないか?
1049 |
1050 |
--------------------------------------------------------------------------------
/03_in_forward_plugin.md:
--------------------------------------------------------------------------------
1 | # Fluentd の in_forward とプラグイン呼び出し
2 |
3 | in_forward をサンプルにした Fluentd 本体のプラグイン呼び出しの流れ
4 |
5 | * [in_forward プラグイン](#in_forward-%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3)
6 | * [in_forward のパラメータたち](#in_forward-%E3%81%AE%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%9F%E3%81%A1)
7 | * [in_forward の構造](#in_forward-%E3%81%AE%E6%A7%8B%E9%80%A0)
8 | * [スレッド(Coolio)周りの処理](#%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89coolio%E5%91%A8%E3%82%8A%E3%81%AE%E5%87%A6%E7%90%86)
9 | * [heartbeat 受信の流れ](#heartbeat-%E5%8F%97%E4%BF%A1%E3%81%AE%E6%B5%81%E3%82%8C)
10 | * [データ受信の流れ](#%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%97%E4%BF%A1%E3%81%AE%E6%B5%81%E3%82%8C)
11 | * [Input プラグイン呼び出しの流れ](#input-%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%81%AE%E6%B5%81%E3%82%8C)
12 | * [Output プラグイン呼び出しの流れ](#output-%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%81%AE%E6%B5%81%E3%82%8C)
13 | * [Input から Output を通した流れまとめ](#input-%E3%81%8B%E3%82%89-output-%E3%82%92%E9%80%9A%E3%81%97%E3%81%9F%E6%B5%81%E3%82%8C%E3%81%BE%E3%81%A8%E3%82%81)
14 | * [まとめおよび補足](#%E3%81%BE%E3%81%A8%E3%82%81%E3%81%8A%E3%82%88%E3%81%B3%E8%A3%9C%E8%B6%B3)
15 |
16 | ## 今回カバーするファイルたち
17 |
18 | * [lib/fluent/plugin/in_forward.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/in_forward.rb)
19 | * [lib/fluent/input.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/input.rb)
20 | * [lib/fluent/supervisor.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/supervisor.rb)
21 | * [lib/fluent/engine.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/engine.rb)
22 |
23 | # in_forward プラグイン
24 |
25 | ## in_forward のパラメータたち
26 |
27 | ドキュメントを見よう => [in_forward](http://docs.fluentd.org/articles/in_forward)
28 |
29 | [in_forward.rb#L29-L33](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/in_forward.rb#L29-L33)
30 |
31 | port 番号指定に使うぐらい。linger_timeout はデフォルトの 0 推奨。
32 |
33 | ```ruby
34 | class ForwardInput < Input
35 | config_param :port, :integer, :default => DEFAULT_LISTEN_PORT
36 | config_param :bind, :string, :default => '0.0.0.0'
37 | config_param :backlog, :integer, :default => nil
38 | # SO_LINGER 0 to send RST rather than FIN to avoid lots of connections sitting in TIME_WAIT at src
39 | config_param :linger_timeout, :integer, :default => 0
40 | ```
41 |
42 | サンプル fluent.conf
43 |
44 |
45 | ```apache
46 |
47 | type forward
48 | port 12000
49 |
50 | ```
51 |
52 | ## in_forward の構造
53 |
54 | それぞれ説明していく
55 |
56 | ```ruby
57 | class ForwardInput < Input
58 | def initialize # new
59 | def configure(conf) # パラメタ設定. start より前に呼ばれる
60 | def start # スレッド初期化. heartbeat とデータ受信
61 | def shutdown # スレッド終了
62 | def listen # listenインスタンス Coolio::TCPServer.new(@bind, @port, Handler) を返す
63 | def run # スレッド開始
64 | def on_message(msg) # Handler#on_read で Msgpack にデータが feed され、Msgpack によって呼ばれる
65 | class Handler < Coolio::Socket
66 | def initialize(io, linger_timeout, log, on_message)
67 | def on_connect # connect されたら呼ばれる。
68 | def on_read(data) # データを受け取ったら呼ばれる
69 | def on_read_json(data) # jsonデータを受け取ったら呼ばれる
70 | def on_read_msgpack(data) # msgpackデータを受け取ったら呼ばれる
71 | def on_close # close されたら呼ばれる
72 | end
73 | class HeartbeatRequestHandler < Coolio::IO
74 | def initialize(io, callback)
75 | def on_readable # heartbeat を受け取ったら呼ばれる. on_heartbeat_request を呼ぶ
76 | end
77 | def on_heartbeat_request(host, port, msg) # heartbeat への返答を返す
78 | end
79 | end
80 | ```
81 |
82 | ## 前提知識: Input プラグインの構造
83 |
84 | [Writing Input Plugins](http://docs.fluentd.org/articles/plugin-development#writing-input-plugins)
85 |
86 | * configure 設定
87 | * start スレッド初期化
88 | * shutdown スレッドストップ
89 | * スレッドから Fluent::Engine.emit(tag, time, record) を呼び出してデータを流す
90 |
91 | [lib/fluent/input.rb](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/input.rb)
92 |
93 | Input クラスを継承して、Input プラグインを作る。
94 |
95 | 実は何も頑張っていない orz
96 |
97 | ```ruby
98 | module Fluent
99 | class Input
100 | include Configurable # config_param を使えるように
101 | include PluginId # config_param :id を定義しているが、そんなに使ってるの見た事ない
102 | include PluginLoggerMixin # v0.10.43 から入った log メソッドを使えるように
103 |
104 | def initialize
105 | super
106 | end
107 |
108 | def configure(conf)
109 | super
110 | end
111 |
112 | def start
113 | end
114 |
115 | def shutdown
116 | end
117 | end
118 | end
119 | ```
120 |
121 | 他のプラグインを参考に、start 内部でスレッドを切り(もしくは Coolio を使って nonblocking にして)、
122 | スレッドで入力待ち受けループを走らせて、
123 | 受け取ったデータは `Fluent::Engine.emit(tag, time, record)` を呼び出して流す、
124 | という処理を書く。
125 |
126 | Coolio まで含めてテンプレ化した親クラス1つ用意しても良さそうな気がするので、気が向いたらやる。かも。
127 |
128 | ## スレッド(Coolio)周りの処理
129 |
130 | `start` と `shutdown`
131 |
132 | `Coolio::TCPServer.new(@bind, @port, Handler)` と `HeartbeatRequestHandler` を Coolio::Loop に attach して、
133 | Thread で Coolio::Loop#start
134 |
135 | ```ruby
136 | def start
137 | @loop = Coolio::Loop.new
138 |
139 | @lsock = listen
140 | @loop.attach(@lsock)
141 |
142 | @usock = SocketUtil.create_udp_socket(@bind)
143 | @usock.bind(@bind, @port)
144 | @usock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
145 | @hbr = HeartbeatRequestHandler.new(@usock, method(:on_heartbeat_request))
146 | @loop.attach(@hbr)
147 |
148 | @thread = Thread.new(&method(:run))
149 | @cached_unpacker = $use_msgpack_5 ? nil : MessagePack::Unpacker.new
150 | end
151 |
152 | def listen
153 | log.info "listening fluent socket on #{@bind}:#{@port}"
154 | s = Coolio::TCPServer.new(@bind, @port, Handler, @linger_timeout, log, method(:on_message))
155 | s.listen(@backlog) unless @backlog.nil?
156 | s
157 | end
158 |
159 | def run
160 | @loop.run
161 | rescue => e
162 | log.error "unexpected error", :error => e, :error_class => e.class
163 | log.error_backtrace
164 | end
165 | ```
166 |
167 | shutdown 時には Handler (watchers) を detach して、stop
168 |
169 | ```ruby
170 | def shutdown
171 | @loop.watchers.each {|w| w.detach }
172 | @loop.stop
173 | @usock.close
174 | listen_address = (@bind == '0.0.0.0' ? '127.0.0.1' : @bind)
175 | # This line is for connecting listen socket to stop the event loop.
176 | # We should use more better approach, e.g. using pipe, fixing cool.io with timeout, etc.
177 | TCPSocket.open(listen_address, @port) {|sock| } # FIXME @thread.join blocks without this line
178 | @thread.join
179 | @lsock.close
180 | end
181 | ```
182 |
183 | ## heartbeat 受信の流れ
184 |
185 | `HeartbeatRequestHandler` 周りの処理については、[01_out_forwrd_heartbeat.md](./01_out_forwrd_heartbeat.md) でカバーしたので飛ばす
186 |
187 | ## データ受信の流れ
188 |
189 | [in_forward.rb#L149-L203](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/in_forward.rb#L149-L203)
190 |
191 | `Coolio::TCPServer.new(@bind, @port, Handler)` 周りの処理。データを受け取ると Coolio::TCPServer に登録した Handler クラスの on_read が呼ばれる。
192 | data は [ForwardOutput#send_data](https://github.com/fluent/fluentd/blob/4aff95cf7b6f11fef4b0b3ad2a9f3e1f6387032e/lib/fluent/plugin/out_forward.rb#L141) が
193 | [tag, chunk] を送信しているので、ここで受け取るのも [tag, chunk] のセット1つのはず。
194 |
195 | ```ruby
196 | class Handler < Coolio::Socket
197 | def initialize(io, linger_timeout, log, on_message)
198 | super(io)
199 | if io.is_a?(TCPSocket)
200 | opt = [1, linger_timeout].pack('I!I!') # { int l_onoff; int l_linger; }
201 | io.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, opt)
202 | end
203 | @on_message = on_message
204 | @log = log
205 | @log.trace {
206 | remote_port, remote_addr = *Socket.unpack_sockaddr_in(@_io.getpeername) rescue nil
207 | "accepted fluent socket from '#{remote_addr}:#{remote_port}': object_id=#{self.object_id}"
208 | }
209 | end
210 |
211 | def on_connect
212 | end
213 |
214 | def on_read(data)
215 | first = data[0]
216 | if first == '{' || first == '['
217 | m = method(:on_read_json)
218 | @y = Yajl::Parser.new
219 | @y.on_parse_complete = @on_message
220 | else
221 | m = method(:on_read_msgpack)
222 | @u = MessagePack::Unpacker.new
223 | end
224 |
225 | # define_method で on_read を on_read_json か on_read_msgpack にすげ換えているので、以降直接呼ばれる
226 | (class << self; self; end).module_eval do
227 | define_method(:on_read, m)
228 | end
229 | m.call(data)
230 | end
231 | ```
232 |
233 | json なら on_read_json が呼ばれて、msgpack なら on_read_msgpack が呼ばれる。
234 |
235 | Msgpack::Unpacker#feed_each については [https://github.com/msgpack/msgpack-ruby](https://github.com/msgpack/msgpack-ruby) の README
236 | または [MessagePack for C/C++ /Ruby アップデート - Blog by Sadayuki Furuhashi](http://frsyuki.hatenablog.com/entry/20100302/p1) を読むと良い。
237 | デシリアライザにネットワークから受け取ったデータを突っ込みながら、obj を取り出せるようになった時点で callback に登録してある on_message メソッドを event driven 的に呼ぶ。
238 | 逆に言えば、obj が取り出せるだけのデータを受け取っていなければ、on_message を呼ばずにすぐに終了する。
239 |
240 | ```ruby
241 | def on_read_json(data)
242 | @y << data
243 | rescue => e
244 | @log.error "forward error", :error => e, :error_class => e.class
245 | @log.error_backtrace
246 | close
247 | end
248 |
249 | def on_read_msgpack(data)
250 | @u.feed_each(data, &@on_message) # デシリアライザに feed しながら、obj 毎に on_message を呼ぶ
251 | rescue => e
252 | @log.error "forward error", :error => e, :error_class => e.class
253 | @log.error_backtrace
254 | close
255 | end
256 |
257 | def on_close
258 | @log.trace { "closed fluent socket object_id=#{self.object_id}" }
259 | end
260 | end
261 | ```
262 |
263 | [in_forward.rb#L112-L147](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/plugin/in_forward.rb#L112-L147)
264 |
265 | 要素毎に on_message メソッドが呼ばれる。Engine.emit_stream(tag, es) または Engine.emit(tag, time, record) を呼び出す。
266 | 結果、tag にマッチする ディレクティブに指定された output プラグインが呼び出される。
267 |
268 | output プラグイン呼び出しの詳細を次の章で詰めていく。
269 |
270 | ```ruby
271 | def on_message(msg)
272 | if msg.nil?
273 | # for future TCP heartbeat_request
274 | return
275 | end
276 |
277 | # TODO format error
278 | tag = msg[0].to_s
279 | entries = msg[1]
280 |
281 | if entries.class == String
282 | # PackedForward
283 | es = MessagePackEventStream.new(entries, @cached_unpacker)
284 | Engine.emit_stream(tag, es)
285 |
286 | elsif entries.class == Array
287 | # Forward
288 | es = MultiEventStream.new
289 | entries.each {|e|
290 | record = e[1]
291 | next if record.nil?
292 | time = e[0].to_i
293 | time = (now ||= Engine.now) if time == 0
294 | es.add(time, record)
295 | }
296 | Engine.emit_stream(tag, es)
297 |
298 | else
299 | # Message
300 | record = msg[2]
301 | return if record.nil?
302 | time = msg[1]
303 | time = Engine.now if time == 0
304 | Engine.emit(tag, time, record)
305 | end
306 | end
307 | ```
308 |
309 | # Fluentd 本体のプラグイン呼び出し
310 |
311 | ## Input プラグイン呼び出しの流れ
312 |
313 | ### [Fluent::Supervisor#start](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/supervisor.rb#L79-L99)
314 |
315 | Fluent::Supervisor は daemon 起動を司るクラス。lib/fluent/command/fluentd.rb で `Fluent::Supervisor.new(opts).start` のように呼ばれる
316 |
317 | ここでプラグイン呼び出しに関して着目すべきは `#run_configure` と `#run_engine`
318 |
319 | ```ruby
320 | def start
321 | require 'fluent/load'
322 | @log.init
323 |
324 | dry_run if @dry_run
325 | start_daemonize if @daemonize
326 | install_supervisor_signal_handlers
327 | until @finished
328 | supervise do
329 | read_config
330 | change_privilege
331 | init_engine
332 | install_main_process_signal_handlers
333 | run_configure
334 | finish_daemonize if @daemonize
335 | run_engine
336 | exit 0
337 | end
338 | $log.error "fluentd main process died unexpectedly. restarting." unless @finished
339 | end
340 | end
341 |
342 | ...
343 |
344 | def run_configure
345 | Fluent::Engine.parse_config(@config_data, @config_fname, @config_basedir)
346 | end
347 |
348 | ...
349 |
350 | def run_engine
351 | Fluent::Engine.run
352 | end
353 | ```
354 |
355 | ### [Fluent::Engine#configure](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/engine.rb#L83-L124)
356 |
357 | 1. conf ファイルの source ディレクティブを読み込んで、in_xxx プラグインを new -> configure。
358 | 1. 同様に match ディレクティブを読み込んで、out_xxx プラグインを new -> configure。
359 |
360 | ```ruby
361 | def parse_config(io, fname, basepath=Dir.pwd)
362 | conf = if fname =~ /\.rb$/
363 | Config::DSL::Parser.parse(io, File.join(basepath, fname))
364 | else
365 | Config.parse(io, fname, basepath)
366 | end
367 | configure(conf)
368 | conf.check_not_fetched {|key,e|
369 | $log.warn "parameter '#{key}' in #{e.to_s.strip} is not used."
370 | }
371 | end
372 |
373 | def configure(conf)
374 | # plugins / configuration dumps
375 | Gem::Specification.find_all.select{|x| x.name =~ /^fluent(d|-(plugin|mixin)-.*)$/}.each do |spec|
376 | $log.info "gem '#{spec.name}' version '#{spec.version}'"
377 | end
378 |
379 | unless @suppress_config_dump
380 | $log.info "using configuration file: #{conf.to_s.rstrip}"
381 | end
382 |
383 | conf.elements.select {|e|
384 | e.name == 'source'
385 | }.each {|e|
386 | type = e['type']
387 | unless type
388 | raise ConfigError, "Missing 'type' parameter on directive"
389 | end
390 | $log.info "adding source type=#{type.dump}"
391 |
392 | input = Plugin.new_input(type) ## 1. 各プラグインの #initialize
393 | input.configure(e) ## 2. 各プラグインの #configure
394 |
395 | @sources << input
396 | }
397 |
398 | conf.elements.select {|e|
399 | e.name == 'match'
400 | }.each {|e|
401 | type = e['type']
402 | pattern = e.arg
403 | unless type
404 | raise ConfigError, "Missing 'type' parameter on directive"
405 | end
406 | $log.info "adding match", :pattern=>pattern, :type=>type
407 |
408 | output = Plugin.new_output(type) ## 1. 各プラグインの #initialize
409 | output.configure(e) ## 2. 各プラグインの #configure
410 |
411 | match = Match.new(pattern, output)
412 | @matches << match
413 | }
414 | end
415 | ```
416 |
417 | ### [FluentEnging#run](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/engine.rb#L203-L236)
418 |
419 | 1. in, out に関わらず、プラグインの start メソッドを呼ぶ。各プラグインはそこで Thread を切るなどする。
420 |
421 | * input プラグインの場合は、起動したスレッドでデータを読む込むコードを書いたりする。
422 | * output プラグインの場合は、スレッドを起動せず、#emit が呼ばれたらなにか処理する、というコードを書いたりする。
423 |
424 | 2. Ctrl-C など stop 要請があったら shutdown メソッドを呼んで後処理。スレッドを閉じるなど。
425 |
426 | ```ruby
427 | def run
428 | begin
429 | start
430 |
431 | if match?($log.tag)
432 | $log.enable_event
433 | @log_emit_thread = Thread.new(&method(:log_event_loop))
434 | end
435 |
436 | unless @engine_stopped
437 | # for empty loop
438 | @default_loop = Coolio::Loop.default
439 | @default_loop.attach Coolio::TimerWatcher.new(1, true)
440 | # attach async watch for thread pool
441 | @default_loop.run
442 | end
443 |
444 | if @engine_stopped and @default_loop
445 | @default_loop.stop
446 | @default_loop = nil
447 | end
448 |
449 | rescue => e
450 | $log.error "unexpected error", :error_class=>e.class, :error=>e
451 | $log.error_backtrace
452 | ensure
453 | $log.info "shutting down fluentd"
454 | shutdown
455 | if @log_emit_thread
456 | @log_event_loop_stop = true
457 | @log_emit_thread.join
458 | end
459 | end
460 | end
461 |
462 | ...
463 |
464 | def start
465 | @matches.each {|m|
466 | m.start
467 | @started << m
468 | }
469 | @sources.each {|s|
470 | s.start
471 | @started << s
472 | }
473 | end
474 |
475 | ...
476 |
477 | def shutdown
478 | @started.map {|s|
479 | Thread.new do
480 | begin
481 | s.shutdown
482 | rescue => e
483 | $log.warn "unexpected error while shutting down", :error_class=>e.class, :error=>e
484 | $log.warn_backtrace
485 | end
486 | end
487 | }.each {|t|
488 | t.join
489 | }
490 | end
491 |
492 | ```
493 |
494 | ## Output プラグイン呼び出しの流れ
495 |
496 | Input プラグイン、または Filter プラグイン(Engine#emit を呼ぶ Output プラグインを便宜上 Filter プラグインと呼ぶことにする。たとえば [fluent-plugin-grep](https://github.com/sonots/fluent-plugin-grep) など)
497 | が Engine#emit を呼び出すと、tag がマッチする 節に指定した Output プラグインの #emit メソッドが呼び出される。
498 |
499 | ### [Engine#emit_stream](https://github.com/fluent/fluentd/blob/9fea4bd69420daf86411937addc6000dfcc6043b/lib/fluent/engine.rb#L130-L163)
500 |
501 | event_stream で match(tag) オブジェクト、つまり output プラグインのインスタンスの emit メソッドを呼び出している。
502 |
503 |
504 | ```ruby
505 | def emit(tag, time, record)
506 | unless record.nil?
507 | emit_stream tag, OneEventStream.new(time, record)
508 | end
509 | end
510 |
511 | def emit_array(tag, array)
512 | emit_stream tag, ArrayEventStream.new(array)
513 | end
514 |
515 | def emit_stream(tag, es)
516 | target = @match_cache[tag]
517 | unless target
518 | target = match(tag) || NoMatchMatch.new
519 | # this is not thread-safe but inconsistency doesn't
520 | # cause serious problems while locking causes.
521 | if @match_cache_keys.size >= MATCH_CACHE_SIZE
522 | @match_cache_keys.delete @match_cache_keys.shift
523 | end
524 | @match_cache[tag] = target
525 | @match_cache_keys << tag
526 | end
527 | target.emit(tag, es)
528 | rescue => e
529 | if @suppress_emit_error_log_interval == 0 || now > @next_emit_error_log_time
530 | $log.warn "emit transaction failed ", :error_class=>e.class, :error=>e
531 | $log.warn_backtrace
532 | # $log.debug "current next_emit_error_log_time: #{Time.at(@next_emit_error_log_time)}"
533 | @next_emit_error_log_time = Time.now.to_i + @suppress_emit_error_log_interval
534 | # $log.debug "next emit failure log suppressed"
535 | # $log.debug "next logged time is #{Time.at(@next_emit_error_log_time)}"
536 | end
537 | raise
538 | end
539 | ```
540 |
541 | ## Input から Output を通した流れまとめ
542 |
543 | 起動時
544 |
545 | 1. config に登場する全プラグインをインスタンス化
546 | 2. 全プラグイン#configure
547 | 3. 全プラグイン#start。ここで通常 Input プラグインはスレッド(Coolio)を切る。
548 |
549 | Input
550 |
551 | 1. Input プラグインのイベントループが入力を受け付ける(on_read)
552 | 2. Input プラグインが、[tag, chunk] ペアの obj を取り出せるだけのデータを受け取っていたら、Engine.emit を呼び出す(on_message)
553 | 3. Fluentd 本体が tag に match する output プラグインを見つけて output#emit を呼び出す
554 |
555 | Output
556 |
557 | 1. output#emit が呼ばれる
558 | 2. HTTP ポストするなど Output プラグインの処理を行う。
559 | 3. 別の Output プラグインを Engine#emit から呼び出している場合は、そちらの output プラグインの #emit 処理に入る。
560 |
561 | 流れの例としては、
562 |
563 | * input#on_read => input#on_message
564 |
565 | * => output1#emit => output2#emit => output3#emit => HTTPポスト
566 |
567 | のようになるだろう ここで注意すべき点は、on_messaage メソッドは、HTTPポスト出力まで終わって、
568 | 始めて終了するため、それらの処理がブロックしている間は、入力受付(on_read呼び出し)ができない、という点である。
569 |
570 | そこで、[スレッドを切って処理する BufferedOutput を継承して使おう](http://chopl.in/blog/2014/01/19/user_bufferedoutput_for_blocking_plugin_instead_of_output.html)
571 | のように BufferedOuput プラグインにして #emit 呼び出しでは enqueue するだけにして、
572 | 別スレッドでデータ処理や HTTP ポストのようなブロックする処理をさせる方法が取られる。
573 |
574 | しかし、今度は捌ききれない量のデータをとりあえず enqueue するようになってしまうため
575 | メモリ上にデータが溜まりまくり、buffer_queue_limit 溢れ、となる問題が発生しうる。
576 | 一長一短である。
577 |
578 |
579 | # まとめおよび補足
580 |
581 | in_forward プラグインをサンプルに、Fluentd 本体のプラグイン呼び出しを解説した。
582 |
583 | ブロックしている時間を可視化するために、[fluent-plugin-elapsed-time](https://github.com/sonots/fluent-plugin-elapsed-time) というプラグインを作ってある。
584 | 上の例で言うと、output1#emit が呼び出されてから、HTTPポストが終わるまでの時間を計測できる。
585 |
586 | より厳密に input#on_read, input#on_message でブロックされている時間を可視化するには in_forward.rb を直接いじる必要がある。
587 | その場合は[こちらのパッチ](https://github.com/sonots/fluentd/commit/364d4e0ec320ee4a44880ee6651a983103af6c71) が利用できる。
588 | => 黒魔術を使って外から hook する方法を思いついたので作って gem 化してみた [fluent-plugin-measure_time](https://github.com/sonots/fluent-plugin-measure_time)
589 |
--------------------------------------------------------------------------------
/04_fluent_agent_lite.md:
--------------------------------------------------------------------------------
1 | # fluent-agent-lite
2 |
3 | Fluentd の agent (ログを読み込んで送信する)としてだけ使える機能を持つ @tagomoris 氏による perl 実装
4 |
5 | ## ディレクトリ構造
6 |
7 | ```
8 | $ tree
9 | .
10 | ├── Makefile.PL
11 | ├── README.md
12 | ├── SPECS
13 | │ └── fluent-agent-lite.spec
14 | ├── bin
15 | │ ├── cpanm
16 | │ ├── fluent-agent-lite
17 | │ └── install.sh
18 | ├── lib
19 | │ └── Fluent
20 | │ └── AgentLite.pm
21 | └── package
22 | ├── fluent-agent-lite.conf
23 | └── fluent-agent-lite.init
24 | ```
25 |
26 | 本体は lib/Fluent/AgentLite.pm だが、
27 | オプションを足すような改修をする場合は大体、
28 | bin/fluent-agent-lite、fluent-agent-lite.init もいじることになる
29 |
30 | 例えば [pull request #22](https://github.com/tagomoris/fluent-agent-lite/pull/22)
31 |
32 | ## コマンドオプション
33 |
34 | bin/fluent-agent-lite コマンドオプション
35 |
36 | ```
37 | /usr/local/fluent-agent-lite/bin/fluent-agent-lite -h
38 | Usage: fluent-agent-lite [options] TAG TARGET_FILE PRIMARY_SERVER[:PORT] [SECONDARY_SERVER[:PORT]]
39 | fluent-agent-lite -p SERVER_LIST_FILE [-s SERVER_LIST_FILE] [options] TAG TARGET_FILE
40 |
41 | port default: 24224
42 | Options:
43 | -f FIELDNAME fieldname of fluentd log message attribute (DEFAULT: message)
44 | -p LIST_PATH primary servers list (server[:port] per line, random selected one server)
45 | -s LIST_PATH secondary servers list (server[:port] per line, random selected one server)
46 | -b BUF_SIZE log tailing buffer size (DEFAULT: 1MB)
47 | -n NICE tail process nice (DEFAULT: 0)
48 | -t TAIL_PATH tail path (DEFAULT: /usr/bin/tail)
49 | -i SECONDS tail -F sleep interval (GNU tail ONLY, DEFAULT: tail default)
50 | -l LOG_PATH log file path (DEFAULT: /tmp/fluent-agent.log)
51 | -P TAG:DATA send a ping message per minute with specified TAG and DATA (DEFAULT: not to send)
52 | (see also: fluent-plugin-ping-message)
53 | -S SECONDS ping message interval seconds (DEFAULT: 60)
54 | -d DRAIN_LOG_TAG emits drain log to fluentd: messages per drain/send (DEFAULT: not to emits)
55 | -k KEEPALIVE_TIME connection keepalive time in seconds. 0 means infinity (DEFAULT: 1800, minimum: 120)
56 | -w RECONNECT_WAIT_MAX the maximum wait time for TCP socket reconnection in seconds (DEFAULT: 3600, minimum: 0.5)
57 | -r RECONNECT_WAIT_INCR_RATE the rate to increment the reconnect time (DEFAULT: 1.5, minimum: 1.0)
58 | -j use JSON for message structure in transfering (highly experimental)
59 | -v output logs of level debug and info (DEFAULT: warn/crit only)
60 | -F force start even if input file is not found
61 | -h print this message
62 | ```
63 |
64 | ## 設定ファイル
65 |
66 | [fluent-agent-lite.conf](https://github.com/tagomoris/fluent-agent-lite/blob/53223a057bff596bbe2a3393b30ec10ca017a9c9/package/fluent-agent-lite.conf)
67 |
68 | ```
69 | # fluentd tag prefix of all LOGS
70 | TAG_PREFIX=""
71 |
72 | # fluentd message log attribute name (default: message)
73 | # FIELD_NAME="message"
74 |
75 | ### LOGS: tag /path/to/log/file
76 | #
77 | # LOGS=$(cat <<"EOF"
78 | # apache2 /var/log/apache2/access.log
79 | # # yourservice /var/log/yourservice/access.log
80 | # EOF
81 | # )
82 |
83 | ### or, read from external file
84 | # LOGS=$(cat /etc/fluent-agent.logs)
85 |
86 | # SERVERNAME[:PORTNUM]
87 | # port number is optional (default: 24224)
88 | PRIMARY_SERVER="primary.fluentd.local:24224"
89 |
90 | ### or, PRIMARY SERVER LIST FILE of servers
91 | # PRIMARY_SERVERS_LIST="/etc/fluent-agent.servers.primary"
92 |
93 | # secondary server setting is optional...
94 | # SECONDARY_SERVER="secondary.fluentd.local:24224"
95 |
96 | # SECONDARY_SERVERS_LIST is available as like as PRIMARY_SERVERS_LIST
97 |
98 | # max bytes to try read as one action from tail (default: 1MB)
99 | # READ_BUFFER_SIZE=1048576
100 |
101 | # PROCESS_NICE default: 0
102 | # PROCESS_NICE=-1
103 |
104 | # TAIL_INTERVAL=0.1
105 | # TAIL_PATH=/usr/bin/tail
106 |
107 | # Tag , data and interval of ping message (not to output ping message when tag not specified)
108 | # fluent-plugin-ping-message を用いた agent の死活監視
109 | # PING_TAG=ping
110 | # PING_DATA=`hostname`
111 | # PING_INTERVAL=60
112 |
113 | # Tag name of 'drain_log' (none: not to output drain_log)
114 | # tail からログを読み込んだ(drainした)という記録(ログ)を fluentd に送信する
115 | # DRAIN_LOG_TAG=
116 |
117 | # connection keepalive time in seconds. 0 means infinity (DEFAULT: 1800)
118 | # KEEPALIVE_TIME=1800
119 |
120 | # The maximum wait time for TCP socket reconnection in seconds (Default: 3600, minimum: 0.5)
121 | # RECONNECT_WAIT_MAX=3600
122 |
123 | # The rate to increment the reconnect time (Default: 1.5)
124 | # RECONNECT_WAIT_INCR_RATE=1.5
125 |
126 | # LOG_PATH=/tmp/fluent-agent.log
127 | # LOG_VERBOSE=false
128 |
129 | # PERL_PATH=/usr/bin/perl
130 | ```
131 |
132 | ↓うちの設定
133 |
134 | この conf は実はシェルスクリプトとして実行されるため、
135 | 起動時に hostname を取得したり、primary_server_list.conf を生成して指定するなどしている!
136 |
137 |
138 | ```bash
139 | # pre-process
140 | hostname=$(hostname)
141 | mkdir -p /etc/fluent-agent-lite
142 | cat <<"EOF" > /etc/fluent-agent-lite/primary_servers_list.conf
143 | ホスト名:ポート番号
144 | ...
145 | ...
146 | EOF
147 | # configuration
148 | TAG_PREFIX=raw
149 | LOGS=$(cat < /dev/null 2>&1
184 | 64 echo $CPID >> $PID_FILE
185 | 65 done
186 | ```
187 |
188 | 蛇足: この disown で daemon 化している所がよくなくて、
189 | ssh -t 経由で fluent-agent-lite を起動しようとするとログアウト後にすぐ fluent-agent-lite プロセスが HUP されて死ぬという問題がある。
190 | See [Issue #16](https://github.com/tagomoris/fluent-agent-lite/issues/16).
191 |
192 | ## bin/fluent-agent-lite
193 |
194 | [bin/fluent-agent-lite](https://github.com/tagomoris/fluent-agent-lite/blob/53223a057bff596bbe2a3393b30ec10ca017a9c9/bin/fluent-agent-lite)
195 |
196 | conf の値を読み込んでいるだけ。と思いきや、もうちょい仕事をしている。
197 | tail コマンドの組み立てや open はこっちでやって AgentLite.pm に渡している
198 |
199 | ```
200 | sub HELP_MESSAGE # ヘルプ
201 | sub server_entry # host:port を [host, port] に split
202 | sub load_server_list # server_list ファイルを読み込む
203 | sub build_tail_command # tail コマンドの引数組み立て
204 | sub open_tail # tail コマンドを nonblocking に pipe open
205 | sub open_stdin # STDIN からログを読み込む場合
206 | sub close_fd # tail or STDIN を閉じる
207 | sub main # open_tail して、AgentLite を new して、execute して、おわったら tail を close_fd
208 | ```
209 |
210 | あと、signal trap もこちらで定義していて、
211 |
212 | [fluent-agent-lite#L21-L33](https://github.com/tagomoris/fluent-agent-lite/blob/53223a057bff596bbe2a3393b30ec10ca017a9c9/bin/fluent-agent-lite#L21-L33)
213 |
214 | ```perl
215 | my $HUPPED = undef;
216 | $SIG{ HUP } = sub { $HUPPED = 1; }; # to reconnect
217 | my $TERMINATED = undef;
218 | $SIG{ INT } = $SIG{ TERM } = sub { $TERMINATED = 1; }; # to terminate
219 |
220 | my $checker_terminated = sub { $TERMINATED; };
221 | my $checker_reconnect = sub {
222 | if (shift) {
223 | $HUPPED = undef;
224 | } else {
225 | $HUPPED or $TERMINATED;
226 | }
227 | };
228 | ```
229 |
230 | AgentLite#execute に無名関数を渡していたりもする。ここで tailfd も渡っている。
231 |
232 | [fluent-agent-lite#L252-L259](https://github.com/tagomoris/fluent-agent-lite/blob/53223a057bff596bbe2a3393b30ec10ca017a9c9/bin/fluent-agent-lite#L252-L259)
233 |
234 | ```perl
235 | $agent->execute( {
236 | fieldname => $fieldname,
237 | tailfd => $tailfd,
238 | checker => {
239 | term => $checker_terminated,
240 | reconnect => $checker_reconnect,
241 | },
242 | } );
243 | ```
244 |
245 | ## AgentLite.pm
246 |
247 | [AgentLite.pm](https://github.com/tagomoris/fluent-agent-lite/blob/53223a057bff596bbe2a3393b30ec10ca017a9c9/lib/Fluent/AgentLite.pm)
248 |
249 | 本体
250 |
251 | ### メソッド一覧
252 |
253 | ```
254 | sub connection_keepalive_time # keepalive time を設定値を元に乱数を使って決定
255 | # class AgentLite
256 | sub new # 初期化. bin/fluent-agent-lite から設定値を受け取る
257 | sub execute # bin/fluent-agent-lite から呼び出される本体
258 | sub drain # tail からログを読み込む
259 | sub pack # ログを msgpack
260 | sub pack_json # ログを json に変換
261 | sub pack_ping_message # ping_message を msgpack
262 | sub pack_drainlog # drainしたというログを msgpack
263 | sub pack_drainlog_json # drainしたというログを json に変換
264 | sub choose # random に送信先 server を選択
265 | sub connect # server に接続
266 | sub send # server にデータ送信
267 | sub close # server への接続を閉じる
268 | ```
269 |
270 | ### [sub new](https://github.com/tagomoris/fluent-agent-lite/blob/53223a057bff596bbe2a3393b30ec10ca017a9c9/lib/Fluent/AgentLite.pm#L41-L67)
271 |
272 | bin/fluent-agent-lite から渡された設定を取り込んでいる。
273 |
274 | ```perl
275 | sub new {
276 | my $this = shift;
277 | my ($tag, $primary_servers, $secondary_servers, $configuration) = @_;
278 | my $self = {
279 | tag => $tag,
280 | servers => {
281 | primary => $primary_servers,
282 | secondary => $secondary_servers,
283 | },
284 | buffer_size => $configuration->{buffer_size},
285 | ping_message => $configuration->{ping_message},
286 | drain_log_tag => $configuration->{drain_log_tag},
287 | keepalive_time => $configuration->{keepalive_time},
288 | reconnect_wait_incr_rate => $configuration->{reconnect_wait_incr_rate},
289 | reconnect_wait_max => $configuration->{reconnect_wait_max},
290 | output_format => $configuration->{output_format},
291 | };
292 |
293 | # デフォルトは msgpack だが、オプションで json で投げるようにもできる
294 | if (defined $self->{output_format} and $self->{output_format} eq 'json') {
295 | *pack = *pack_json;
296 | *pack_drainlog = *pack_drainlog_json;
297 | }
298 |
299 | srand (time ^ $PID ^ unpack("%L*", `ps axww | gzip`));
300 |
301 | bless $self, $this;
302 | }
303 | ```
304 |
305 | ### [sub execute](https://github.com/tagomoris/fluent-agent-lite/blob/53223a057bff596bbe2a3393b30ec10ca017a9c9/lib/Fluent/AgentLite.pm#L69-L207)
306 |
307 | 長いのでちょっと分割して読む
308 |
309 | この辺では、受け取った引数から変数を初期化したり、[上にある](https://github.com/tagomoris/fluent-agent-lite/blob/53223a057bff596bbe2a3393b30ec10ca017a9c9/lib/Fluent/AgentLite.pm#L209-L255)定数達から変数を初期化したりしているだけ
310 |
311 | ```perl
312 | sub execute {
313 | my $self = shift;
314 | my $args = shift;
315 |
316 | my $fieldname = $args->{fieldname};
317 | my $tailfd = $args->{tailfd};
318 |
319 | my $check_terminated = ($args->{checker} || {})->{term} || sub { 0 };
320 | my $check_reconnect = ($args->{checker} || {})->{reconnect} || sub { 0 };
321 |
322 | my $packer = Data::MessagePack->new();
323 |
324 | my $reconnect_wait = RECONNECT_WAIT_MIN;
325 | my $reconnect_wait_max = RECONNECT_WAIT_MAX;
326 | if (defined $self->{reconnect_wait_max}) {
327 | $reconnect_wait_max = $self->{reconnect_wait_max};
328 | if ($reconnect_wait_max < RECONNECT_WAIT_MIN) {
329 | warnf 'Reconnect wait max is too short. Set minimum value %s', RECONNECT_WAIT_MIN;
330 | $reconnect_wait_max = RECONNECT_WAIT_MIN;
331 | }
332 | }
333 | my $reconnect_wait_incr_rate = RECONNECT_WAIT_INCR_RATE;
334 | if (defined $self->{reconnect_wait_incr_rate}) {
335 | $reconnect_wait_incr_rate = $self->{reconnect_wait_incr_rate};
336 | if ($reconnect_wait_incr_rate < RECONNECT_WAIT_INCR_RATE_MIN) {
337 | warnf 'Reconnect wait incr rate is too small. Set minimum value %s', RECONNECT_WAIT_INCR_RATE_MIN;
338 | $reconnect_wait_incr_rate = RECONNECT_WAIT_INCR_RATE_MIN;
339 | }
340 | }
341 |
342 | my $last_ping_message = time;
343 | if ($self->{ping_message}) {
344 | $last_ping_message = time - $self->{ping_message}->{interval} * 2;
345 | }
346 | my $keepalive_time = CONNECTION_KEEPALIVE_TIME;
347 | if (defined $self->{keepalive_time}) {
348 | $keepalive_time = $self->{keepalive_time};
349 | if ($keepalive_time < CONNECTION_KEEPALIVE_MIN and $keepalive_time != CONNECTION_KEEPALIVE_INFINITY) {
350 | warnf 'Keepalive time setting is too short. Set minimum value %s', CONNECTION_KEEPALIVE_MIN;
351 | $keepalive_time = CONNECTION_KEEPALIVE_MIN;
352 | }
353 | }
354 |
355 | my $pending_packed;
356 | my $continuous_line;
357 | my $disconnected_primary = 0;
358 | my $expiration_enable = $keepalive_time != CONNECTION_KEEPALIVE_INFINITY;
359 | ```
360 |
361 | メインループ。SIGINT or SIGTERM されるまでループを続ける
362 |
363 | ```perl
364 | while(not $check_terminated->()) {
365 | # at here, connection initialized (after retry wait if required)
366 |
367 | # connect to servers
368 | my $primary = $self->choose($self->{servers}->{primary});
369 | my $secondary;
370 |
371 | my $sock = $self->connect($primary) unless $disconnected_primary;
372 | if (not $sock and $self->{servers}->{secondary}) {
373 | $secondary = $self->choose($self->{servers}->{secondary});
374 | $sock = $self->connect($self->choose($self->{servers}->{secondary}));
375 | }
376 | $disconnected_primary = 0;
377 | # retry ループ
378 | # 実は retry 最中はこのループ内の処理しかされず、tail 子プロセスの監視がされないという問題あったりする
379 | # See https://github.com/tagomoris/fluent-agent-lite/issues/23
380 | # nonblocking sleep にして他の処理もやりつつ reconnect_wait を待つようにする必要がある
381 | unless ($sock) {
382 | # failed to connect both of primary / secondary
383 | # Fluentd server に負荷がかかっているなどしてつながらない場合、この warn が出る
384 | warnf 'failed to connect servers, primary: %s, secondary: %s', $primary, ($secondary || 'none');
385 | warnf 'waiting %s seconds to reconnect', $reconnect_wait;
386 |
387 | Time::HiRes::sleep($reconnect_wait);
388 | $reconnect_wait *= $reconnect_wait_incr_rate;
389 | $reconnect_wait = $reconnect_wait_max if $reconnect_wait > $reconnect_wait_max;
390 | next;
391 | }
392 |
393 | # succeed to connect. set keepalive disconnect time
394 | my $connecting = $secondary || $primary;
395 |
396 | my $expired = time + connection_keepalive_time($keepalive_time) if $expiration_enable;
397 | $reconnect_wait = RECONNECT_WAIT_MIN;
398 | ```
399 |
400 | データ読み込み送信ループ。
401 |
402 | ```perl
403 | # SIGHUP が送られた場合 reconnect するために一旦ループを抜ける
404 | while(not $check_reconnect->()) {
405 | # connection keepalive expired
406 | if ($expiration_enable and time > $expired) {
407 | infof "connection keepalive expired.";
408 | last;
409 | }
410 |
411 | # ping message (if enabled)
412 | my $ping_packed = undef;
413 | if ($self->{ping_message} and time >= $last_ping_message + $self->{ping_message}->{interval}) {
414 | $ping_packed = $self->pack_ping_message($packer, $self->{ping_message}->{tag}, $self->{ping_message}->{data});
415 | $last_ping_message = time;
416 | }
417 |
418 | # drain (sysread)
419 | my $lines = 0;
420 | # 送信されていないデータがない場合、tail から読み込みを試みる(drainする)
421 | if (not $pending_packed) {
422 | my $buffered_lines;
423 | ($buffered_lines, $continuous_line, $lines) = $self->drain($tailfd, $continuous_line);
424 |
425 | if ($buffered_lines) {
426 | # msgpack にパックする
427 | $pending_packed = $self->pack($packer, $fieldname, $buffered_lines);
428 | # drain_log オプションが有効の場合、drain したよ記録(ログ)も pack する
429 | if ($self->{drain_log_tag}) {
430 | $pending_packed .= $self->pack_drainlog($packer, $self->{drain_log_tag}, $lines);
431 | }
432 | }
433 | # ping オプションが有効の場合、くっつける
434 | if ($ping_packed) {
435 | $pending_packed ||= '';
436 | $pending_packed .= $ping_packed;
437 | }
438 | unless ($pending_packed) {
439 | Time::HiRes::sleep READ_WAIT;
440 | next;
441 | }
442 | }
443 | # send
444 | my $written = $self->send($sock, $pending_packed);
445 | unless ($written) { # failed to write (socket error).
446 | $disconnected_primary = 1 unless $secondary;
447 | last; # 失敗した場合は $pending_packed をリセットせずに last して別のノードに reconnect
448 | }
449 |
450 | # 成功した場合は $pending_packed をリセット。
451 | $pending_packed = undef;
452 | }
453 | ```
454 |
455 | reconnect 処理
456 |
457 | ```perl
458 | if ($check_reconnect->()) {
459 | infof "SIGHUP (or SIGTERM) received";
460 | $disconnected_primary = 0;
461 | $check_reconnect->(1); # clear SIGHUP signal
462 | }
463 | infof "disconnecting to current server";
464 | if ($sock) {
465 | $sock->close;
466 | $sock = undef;
467 | }
468 | infof "disconnected.";
469 | }
470 | if ($check_terminated->()) {
471 | warnf "SIGTERM received";
472 | }
473 | infof "process exit";
474 | }
475 | ```
476 |
477 | ### [sub drain](https://github.com/tagomoris/fluent-agent-lite/blob/53223a057bff596bbe2a3393b30ec10ca017a9c9/lib/Fluent/AgentLite.pm#L209-L255)
478 |
479 | tail パイプから読み込む
480 |
481 | ```perl
482 | sub drain {
483 | # if broken child process (undefined return value of $fd->sysread())
484 | # if content exists, return it.
485 | # else die
486 | my ($self,$fd, $continuous_line) = @_;
487 | my $readlimit = $self->{buffer_size};
488 | my $readsize = 0;
489 | my $readlines = 0;
490 | my @buffered_lines;
491 |
492 | my $chunk;
493 | while ($readsize < $readlimit) {
494 | my $bytes = $fd->sysread($chunk, $readlimit);
495 | if (defined $bytes and $bytes == 0) { # EOF (child process exit)
496 | last if $readsize > 0;
497 | warnf "failed to read from child process, maybe killed.";
498 | # tail プロセスが死んだ場合は、ここで fluent-agent-lite も死ぬ
499 | confess "give up to read tailing fd, see logs";
500 | }
501 | if (not defined $bytes and $! == EAGAIN) { # I/O Error (no data in fd): "Resource temporarily unavailable"
502 | last;
503 | }
504 | if (not defined $bytes) { # Other I/O error... what?
505 | warnf "I/O error with tail fd: $!";
506 | last;
507 | }
508 |
509 | $readsize += $bytes;
510 | my $terminated_line = chomp $chunk;
511 | my @lines = split(m!\n!, $chunk);
512 | if ($continuous_line) {
513 | $lines[0] = $continuous_line . $lines[0];
514 | $continuous_line = undef;
515 | }
516 | if (not $terminated_line) {
517 | $continuous_line = pop @lines;
518 | }
519 | if (scalar(@lines) > 0) {
520 | push @buffered_lines, @lines;
521 | $readlines += scalar(@lines);
522 | }
523 | }
524 | if ($readlines < 1) {
525 | return undef, $continuous_line, 0;
526 | }
527 |
528 | return (\@buffered_lines, $continuous_line, $readlines);
529 | }
530 | ```
531 |
532 | ### [sub send](https://github.com/tagomoris/fluent-agent-lite/blob/53223a057bff596bbe2a3393b30ec10ca017a9c9/lib/Fluent/AgentLite.pm#L318-L344)
533 |
534 | ```perl
535 | sub send {
536 | my ($self,$sock,$data) = @_;
537 | my $length = length($data);
538 | my $written = 0;
539 | my $retry = 0;
540 |
541 | local $SIG{"PIPE"} = sub { die $! };
542 |
543 | eval {
544 | while ($written < $length) {
545 | my $wbytes = $sock->syswrite($data, $length, $written);
546 | if ($wbytes) {
547 | $written += $wbytes;
548 | }
549 | else {
550 | die "failed $retry times to send data: $!" if $retry > SEND_RETRY_MAX;
551 | $retry += 1;
552 | }
553 | }
554 | };
555 | if ($@) {
556 | my $error = $@;
557 | # 送信が失敗した場合は、このログが出る
558 | # 2013-11-21T21:11:21 [WARN] (21263) Cannot send data: パイプが切断されました at /usr/local/fluent-agent-lite/bin/../lib/Fluent/AgentLite.pm line 305.\n at /usr/local/fluent-agent-lite/bin/../lib/Fluent/AgentLite.pm line 321
559 | warnf "Cannot send data: $error";
560 | return undef;
561 | }
562 | $written;
563 | }
564 | ```
565 |
566 | ## まとめというか補足
567 |
568 | * init スクリプト内でログの数だけ fluent-agent-lite を起動して disown でデーモン化している
569 | * tail コマンドを open してログを読み込んでいる。結果、Fluentd のようにログ position を覚えたりなどさせるのが難しい。
570 | * Fluentd out_forward のように heartbeat は送っていない。送信に失敗したらランダムで別のノードに reconnect してみるだけ
571 | * Fluentd out_forward と違って keepalive 接続してログを送っている
572 | * Fluentd への接続が不安定になった場合は `Cannot send data` か `failed to connect servers` の warn ログが出る
573 | * 送信が失敗している間は新たに tail から読み込むようなことはない。
574 |
575 | * Fluentd は out_forward が失敗していも、in_tail は別プラグインであるため感知できず、読み込み続けてメモリ溢れしてしまう
576 |
577 | * パフォーマンス検証すると、fluent-agent-lite 52万/sec。Fluentd 7万/sec (しかも詰まる)
578 |
579 | 機能的に足りない点などはあるが、パフォーマンスが素晴らしい
580 |
--------------------------------------------------------------------------------
/05_coolio_cextension.md:
--------------------------------------------------------------------------------
1 | # Cool.io と Ruby C拡張 と GVL とイベント駆動と
2 |
3 | Fluentd が使っているイベント駆動ライブラリである [cool.io](https://github.com/tarcieri/cool.io) をカバーする。
4 |
5 | なお、オリジナル作者である [Tony](https://github.com/tarcieri) は [Celluloid IO](https://github.com/celluloid/celluloid-io) を使ってね、
6 | と言っているので、普通はそちらを使うべきなのだろうが、以前 Celluloid を試した時にパフォーマンスが落ちたため、Fluentd では引き続き cool.io を使っている。
7 |
8 | Cool.io は内部的にはマルチプラットフォームイベント駆動ライブラリである libev
9 | を利用し、ruby から呼べるようにC拡張APIでラップしているので、そちらから入門する
10 |
11 | * [Ruby C拡張](#ruby-c%E6%8B%A1%E5%BC%B5)
12 | * [Ruby C拡張を含んだ gem の作り方](#ruby-c%E6%8B%A1%E5%BC%B5%E3%82%92%E5%90%AB%E3%82%93%E3%81%A0-gem-%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9)
13 | * [Ruby C拡張を含んだ gem のビルド方法](#ruby-c%E6%8B%A1%E5%BC%B5%E3%82%92%E5%90%AB%E3%82%93%E3%81%A0-gem-%E3%81%AE%E3%83%93%E3%83%AB%E3%83%89%E6%96%B9%E6%B3%95)
14 | * [C拡張ライブラリの基礎](#c%E6%8B%A1%E5%BC%B5%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E5%9F%BA%E7%A4%8E)
15 | * [C拡張ライブラリで SEGV したときのデバグ手法](#c%E6%8B%A1%E5%BC%B5%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%A7-segv-%E3%81%97%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AE%E3%83%87%E3%83%90%E3%82%B0%E6%89%8B%E6%B3%95)
16 | * [Cool.io の基礎](#coolio-%E3%81%AE%E5%9F%BA%E7%A4%8E)
17 | * [イベント駆動とか言う前に GVL を知りやがれ](#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E9%A7%86%E5%8B%95%E3%81%A8%E3%81%8B%E8%A8%80%E3%81%86%E5%89%8D%E3%81%AB-gvl-%E3%82%92%E7%9F%A5%E3%82%8A%E3%82%84%E3%81%8C%E3%82%8C)
18 | * [libev のサンプル](#libev-%E3%81%AE%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB)
19 | * [Cool.io のサンプル](#coolio-%E3%81%AE%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB)
20 | * [ノンブロッキング待ち受け vs ブロッキング待ち受け](#%E3%83%8E%E3%83%B3%E3%83%96%E3%83%AD%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0%E5%BE%85%E3%81%A1%E5%8F%97%E3%81%91-vs-%E3%83%96%E3%83%AD%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0%E5%BE%85%E3%81%A1%E5%8F%97%E3%81%91)
21 | * [Fluentd でみる Cool.io の実例](#fluentd-%E3%81%A7%E3%81%BF%E3%82%8B-coolio-%E3%81%AE%E5%AE%9F%E4%BE%8B)
22 | * [Cool.io の構造](#coolio-%E3%81%AE%E6%A7%8B%E9%80%A0)
23 | * [ファイル構造](#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E6%A7%8B%E9%80%A0)
24 | * [クラス構造](#%E3%82%AF%E3%83%A9%E3%82%B9%E6%A7%8B%E9%80%A0)
25 | * [イベント発火の流れ](#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E7%99%BA%E7%81%AB%E3%81%AE%E6%B5%81%E3%82%8C)
26 | * [イベント受信の流れ](#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E5%8F%97%E4%BF%A1%E3%81%AE%E6%B5%81%E3%82%8C)
27 | * [まとめ](#%E3%81%BE%E3%81%A8%E3%82%81)
28 |
29 | # Ruby C拡張
30 |
31 | C拡張API でC言語ライブラリをラップして ruby から呼べるようにできる。
32 |
33 | ## Ruby C拡張を含んだ gem の作り方
34 |
35 | ゼロから作る場合は [BundlerでC拡張を含んだgemを公開する - Qiita [キータ]](http://qiita.com/gam0022/items/2ee82e84e5c9f608eb85) の記事が参考になった。
36 |
37 | ポイントは、ext ディレクトリの下にCソースコードおよび extconf.rb を作成して、
38 |
39 | ```ruby
40 | #extconf.rb
41 | require "mkmf"
42 | create_makefile("cool.io_ext")
43 | ```
44 |
45 | gemspec に
46 |
47 | ```ruby
48 | spec.extensions = %w[ext/cool.io/extconf.rb]
49 | ```
50 |
51 | の行を足すことぐらい。
52 | extconf.rb は Makefile を生成する ruby スクリプトで、
53 | こうしておくと gem install の時に、C拡張を make でコンパイル、ビルドしてからインストールしてくれるようになる。
54 |
55 |
56 | ディレクトリ構造概観
57 |
58 | ```
59 | $ tree
60 | .
61 | ├── cool.io.gemspec
62 | ├── ext
63 | │ ├── cool.io
64 | │ │ ├── cool.io.h
65 | │ │ ├── cool.io_ext.c
66 | │ │ ├── extconf.rb
67 | ```
68 |
69 | ## Ruby C拡張を含んだ gem のビルド方法
70 |
71 | ### gemspec の動作確認
72 |
73 | gem install 時に期待通りにインストールされるか確認する。
74 |
75 | gem をビルドする
76 |
77 | ```bash
78 | $ gem build cool.io.gemspec
79 | ```
80 |
81 | gem をインストールする
82 |
83 | ```bash
84 | $ gem install cool.io-1.2.1.gem
85 | ```
86 |
87 | rbenv で ruby 2.1.1 を使っている場合であれば、`~/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/extensions`
88 | あたりに .so ファイル含めてインストールされる
89 |
90 | require できるか確認
91 |
92 | ```bash
93 | $ irb
94 | irb> require 'cool.io'
95 | => true
96 | ```
97 |
98 | ### Makefile の確認
99 |
100 | extconf.rb を ruby スクリプトとして実行すると Makefile が生成されるので、それで確認できる
101 |
102 | ```bash
103 | $ ruby ext/cool.io/extconf.rb
104 | ```
105 |
106 | ビルドオプションを一時的にいじりたい場合は、Makefile を編集して make コマンドを打つと良い。
107 |
108 | ```bash
109 | $ vi Makefile
110 | $ make
111 | ```
112 |
113 | たとえば、CFLAGS に --save-temps を足して make するとプリプロセッサを通した結果の .i ファイルなどが作られて捗る。
114 |
115 | あとは、gdb で追う時のために最適化オプションを切って `-O0 -ggdb3` にしておくとか。
116 |
117 | ビルドした .so ファイルはワーキングディレクトリの lib 以下に置かれて require できるようになる。
118 |
119 | ### rake compiler の利用
120 |
121 | [rake-compiler](https://rubygems.org/gems/rake-compiler) という gem があって、
122 | これを使うと `rake compile` でビルドできるようになる。
123 | 開発中に便利。こちらもワーキングディレクトリの lib 以下に .so ファイルが置かれる。
124 |
125 | gemspec に development_dependency を足し、
126 |
127 | ```
128 | s.add_development_dependency "rake-compiler"
129 | ```
130 |
131 | Rakefile で require の行を足しておく
132 |
133 | ```ruby
134 | require 'rake/extensiontask'
135 | ```
136 |
137 | これで
138 |
139 | ```bash
140 | $ rake compile
141 | ```
142 |
143 | でC拡張をビルドしてくれるようになる。
144 |
145 | ```bash
146 | $ rake clean
147 | ```
148 |
149 | で make clean 相当もできる。
150 |
151 |
152 | ## C拡張ライブラリの基礎
153 |
154 | こちらの ruby レポジトリに入っているドキュメントが1次情報源である [ruby/ruby/README.EXT.ja](https://github.com/ruby/ruby/blob/trunk/README.EXT.ja)
155 |
156 |
157 | 例えば、[cool.io/ext/loop.c](https://github.com/sonots/cool.io/blob/7453ed1ff1e20de4c99002e24407fcacdb0ad081/ext/cool.io/loop.c) をサンプルに、
158 | C言語ライブラリをどのようにして Ruby のC拡張APIでラップしていくのか学んでみよう。
159 |
160 | [ext/cool.io/loop.c#L44-L54](https://github.com/sonots/cool.io/blob/7453ed1ff1e20de4c99002e24407fcacdb0ad081/ext/cool.io/loop.c#L44-L54)
161 |
162 | `rb_define_method` が ruby での `def` に相当していて、これでCレベルの関数をメソッドとして登録している。
163 | `rb_define_private_method` は private メソッドになる。
164 |
165 | `rb_define_method` の引数の数値は、ruby メソッドの引数の数を表していて、
166 | この例では `initialize` メソッドは引数0個、`ev_loop_new` は引数0個、
167 | `run_once` メソッドは可変長引数、`run_nonblock` メソッドは引数0個であることを表現している。
168 |
169 | ```c
170 | void Init_coolio_loop()
171 | {
172 | mCoolio = rb_define_module("Coolio");
173 | cCoolio_Loop = rb_define_class_under(mCoolio, "Loop", rb_cObject);
174 | rb_define_alloc_func(cCoolio_Loop, Coolio_Loop_allocate);
175 |
176 | rb_define_method(cCoolio_Loop, "initialize", Coolio_Loop_initialize, 0);
177 | rb_define_private_method(cCoolio_Loop, "ev_loop_new", Coolio_Loop_ev_loop_new, 1);
178 | rb_define_method(cCoolio_Loop, "run_once", Coolio_Loop_run_once, -1);
179 | rb_define_method(cCoolio_Loop, "run_nonblock", Coolio_Loop_run_nonblock, 0);
180 | }
181 | ```
182 |
183 | [ext/cool.io/loop.c#L56-L68](https://github.com/sonots/cool.io/blob/7453ed1ff1e20de4c99002e24407fcacdb0ad081/ext/cool.io/loop.c#L56-L68)`
184 |
185 | ruby レベルでは `#new` がメモリ割当てと初期化を兼ねているが、Cレベルでは `allocate` と `initialize` にわかれている。
186 |
187 | ```c
188 | static VALUE Coolio_Loop_allocate(VALUE klass)
189 | {
190 | struct Coolio_Loop *loop = (struct Coolio_Loop *)xmalloc(sizeof(struct Coolio_Loop));
191 |
192 | loop->ev_loop = 0;
193 | ev_init(&loop->timer, Coolio_Loop_timeout_callback);
194 | loop->running = 0;
195 | loop->events_received = 0;
196 | loop->eventbuf_size = DEFAULT_EVENTBUF_SIZE;
197 | loop->eventbuf = (struct Coolio_Event *)xmalloc(sizeof(struct Coolio_Event) * DEFAULT_EVENTBUF_SIZE);
198 |
199 | return Data_Wrap_Struct(klass, Coolio_Loop_mark, Coolio_Loop_free, loop);
200 | }
201 | ```
202 |
203 | ```c
204 | static VALUE Coolio_Loop_initialize(VALUE self)
205 | {
206 | Coolio_Loop_ev_loop_new(self, INT2NUM(0));
207 | }
208 | ```
209 |
210 | [ext/cool.io/loop.c#L90-L102](https://github.com/sonots/cool.io/blob/7453ed1ff1e20de4c99002e24407fcacdb0ad081/ext/cool.io/loop.c#L90-L102)
211 |
212 | `#ev_loop_new` メソッドの引数の数は Init_coolio_loop で定義されていたように、引数1つであり、それが flags であることがわかる。
213 |
214 | Rubyのデータは全て VALUE 型になっているので、Cレベルで利用するためには NUM2INT のような ruby のマクロを使って型変換する必要がある。
215 |
216 | また、Ruby レベルでのオブジェクト (Data オブジェクト) からメンバを取り出す(ポインタを取り出す)には、`Data_Get_Struct` のようなマクロを利用する。
217 |
218 | ```c
219 | /* Wrapper for populating a Coolio_Loop struct with a new event loop */
220 | static VALUE Coolio_Loop_ev_loop_new(VALUE self, VALUE flags)
221 | {
222 | struct Coolio_Loop *loop_data;
223 | Data_Get_Struct(self, struct Coolio_Loop, loop_data);
224 |
225 | if(loop_data->ev_loop)
226 | rb_raise(rb_eRuntimeError, "loop already initialized");
227 |
228 | loop_data->ev_loop = ev_loop_new(NUM2INT(flags));
229 |
230 | return Qnil;
231 | }
232 | ```
233 |
234 | [ext/cool.io/loop.c#L269-L274](https://github.com/sonots/cool.io/blob/7453ed1ff1e20de4c99002e24407fcacdb0ad081/ext/cool.io/loop.c#L269-L274)
235 |
236 | `#run_nonblock` は引数0個のメソッドである。
237 |
238 | ```c
239 | static VALUE Coolio_Loop_run_nonblock(VALUE self)
240 | {
241 | struct Coolio_Loop *loop_data;
242 | VALUE nevents;
243 |
244 | Data_Get_Struct(self, struct Coolio_Loop, loop_data);
245 | ```
246 |
247 | [ext/cool.io/loop.c#L186-L198](https://github.com/sonots/cool.io/blob/7453ed1ff1e20de4c99002e24407fcacdb0ad081/ext/cool.io/loop.c#L186-L198)
248 |
249 | `#run_once` は rb_define_method で -1 と指定されていた可変長引数のメソッドで、
250 | その場合の関数のシグネチャは次のようになる。
251 |
252 | `rb_scan_args` を使って、argv から値を取り出すことになる。
253 | `"01"` の10の位が必須引数の数、1の位がオプション引数の数を表現している。
254 | 今回の場合は、オプション引数が1つ、ということになる。
255 |
256 | ```c
257 | static VALUE Coolio_Loop_run_once(int argc, VALUE *argv, VALUE self)
258 | {
259 | VALUE timeout;
260 | VALUE nevents;
261 | struct Coolio_Loop *loop_data;
262 |
263 | rb_scan_args(argc, argv, "01", &timeout);
264 |
265 | if(timeout != Qnil && NUM2DBL(timeout) < 0) {
266 | rb_raise(rb_eArgError, "time interval must be positive");
267 | }
268 |
269 | Data_Get_Struct(self, struct Coolio_Loop, loop_data);
270 | ```
271 |
272 | ## C拡張ライブラリで SEGV したときのデバグ手法
273 |
274 | Cを直接触っているので SEGV しやすい。SEGV したときの追い方を 解説というよりはメモにすぎないが、残しておく。
275 |
276 | gcc のオプションを -O0 -ggdb3 にしたほうが SEGV 追いやすいとのことなので、Makefile 吐いていじる
277 |
278 | ```
279 | $ ruby ext/cool.io/extconf.rb
280 | $ vim Makefile
281 | ```
282 |
283 | おまけ:Makefile いじって gcc のオプションに --savetemps を足すと、プリコンパイルを通した結果の .i ファイルが作られる。プリコンパイル結果を追いたい場合捗る。
284 |
285 | コア吐かせる準備をしておく。
286 |
287 | ```bash
288 | $ ulimit -n unlimited
289 | ```
290 |
291 | プログラム実行して SEGV 発生させる。すると core.{pid} なファイルができているはず。
292 |
293 | ruby のパスと core ファイルを指定して gdb を起動し、backtrace を表示する
294 |
295 | ```bash
296 | $ gdb `rbenv which ruby` core.2101
297 | (gdb) bt
298 | #0 0x00d98410 in __kernel_vsyscall ()
299 | #1 0x00138df0 in raise () from /lib/libc.so.6
300 | #2 0x0013a701 in abort () from /lib/libc.so.6
301 | #3 0x006a0877 in rb_bug (fmt=0x6d19b5 "Segmentation fault") at error.c:309
302 | #4 0x005c0e86 in sigsegv (sig=11, info=0x959461c, ctx=0x959469c) at signal.c:672
303 | #5
304 | #6 ev_feed_event (loop=0x96a08e0, w=0xb7c0a844, revents=256) at ../../../../ext/cool.io/../libev/ev.c:1702
305 | #7 0x004b0eeb in timers_reify (loop=0x96a08e0, flags=2) at ../../../../ext/cool.io/../libev/ev.c:1725
306 | #8 ev_run (loop=0x96a08e0, flags=2) at ../../../../ext/cool.io/../libev/ev.c:3460
307 | #9 0x004a9d3d in ev_loop (argc=1, argv=0xb7c8d01c, self=154926840) at ../../../../ext/cool.io/../libev/ev.h:826
308 | #10 Coolio_Loop_run_once (argc=1, argv=0xb7c8d01c, self=154926840) at ../../../../ext/cool.io/loop.c:226
309 | #11 0x006269b5 in call_cfunc_m1 (func=0x4a9c50 , recv=154926840, argc=1, argv=0xb7c8d01c) at vm_insnhelper.c:1325
310 | #12 0x0062c22c in vm_call_cfunc_with_frame (th=0x96fca08, reg_cfp=0xb7d0cfb8, ci=) at vm_insnhelper.c:1469
311 | #13 0x0063d40d in vm_exec_core (th=0x96fca08, initial=) at insns.def:1017
312 | #14 0x00642b27 in vm_exec (th=0x96fca08) at vm.c:1201
313 | #15 0x00643ecb in invoke_block_from_c (th=0x96fca08, proc=0x9645ec8, self=154913200, defined_class=147225780, argc=0, argv=0x93bfc6c, blockptr=0x0) at vm.c:648
314 | #16 vm_invoke_proc (th=0x96fca08, proc=0x9645ec8, self=154913200, defined_class=147225780, argc=0, argv=0x93bfc6c, blockptr=0x0) at vm.c:696
315 | #17 0x00657e33 in thread_start_func_2 (th=0x96fca08, stack_start=) at thread.c:512
316 | #18 0x0065804e in thread_start_func_1 (th_ptr=0x96fca08) at thread_pthread.c:765
317 | #19 0x0083c852 in start_thread () from /lib/libpthread.so.0
318 | #20 0x001e3a8e in clone () from /lib/libc.so.6
319 | ```
320 |
321 | sigsegv がおきた次のフレームに移動して変数の中身などを表示する
322 |
323 | ```bash
324 | (gdb) frame 6
325 | #6 ev_feed_event (loop=0x96a08e0, w=0xb7c0a844, revents=256) at ../../../../ext/cool.io/../libev/ev.c:1702
326 | 1702 pendings [pri][w_->pending - 1].events |= revents;
327 | (gdb) p w
328 | $1 = (void *) 0xb7c0a844
329 | (gdb) p pendings
330 | $2 = {0x0, 0x0, 0x9687ed8, 0x0, 0x0}
331 | (gdb) p pri
332 | $3 = 6832926
333 | (gdb) p w_->pending
334 | Cannot access memory at address 0x4
335 | ```
336 |
337 | 0x4 は ruby の nil らしいので、そこがおかしい
338 |
339 | # Cool.io の基礎
340 |
341 | 語弊を恐れずに言うと、Cool.io はマルチプラットフォーム対応イベント駆動ライブラリである libev の Ruby C拡張ラッパーと言える。
342 | ので、cool.io を知るには libev を知る必要があるので触れる。
343 |
344 | もっというと、各プラットフォームごとのイベント駆動APIを知る必要がある(select、poll、 Linux なら epoll、BSD なら kqueue とか)が、
345 | 今回はそこまで深くは追求しない。
346 |
347 | ## イベント駆動とか言う前に GVL を知りやがれ
348 |
349 | libev に入る前に ruby の GVL を勉強しておこう。
350 |
351 | cf. http://docs.ruby-lang.org/ja/1.9.3/class/Thread.html
352 |
353 | > 現在の実装では Ruby VM は Giant VM lock (GVL) を有しており、
354 | 同時に実行される ネイティブスレッドは常にひとつです。
355 | ただし、IO 関連のブロックする可能性があるシステムコールを行う場合には
356 | GVL を解放します。その場合にはスレッドは同時に実行され得ます。
357 | また拡張ライブラリから GVL を操作できるので、複数のスレッドを
358 | 同時に実行するような拡張ライブラリは作成可能です。
359 |
360 | cf. http://d.hatena.ne.jp/kyab/20140215/1392485665
361 |
362 | > CRubyのThreadクラスは1.8.xまではグリーンスレッドで実装されていました。で、1.9.0ではインタプリタからVMになって、ThreadクラスもNative Thread(OSが提供してるスレッド)で実装されるようになったというのは有名な話。
363 |
364 | > ただNative Threadになったけど、マルチコアを(ほとんど)活かせない。なぜならVMが動く際、基本的に一つの巨大な排他をしてるから。この排他というかロックをGVL(Giant VM Lock)またはGIL(Giant Interpreter Lock)と呼ぶ。Native ThreadだからVMが検知できないタイミングでスレッドは切り替わろうとするけど、ロックされてるからまた元のスレッドに戻ってくる(正確にはロックを待っている方のスレッドはOSのスケジューラのキューに入らない)。
365 |
366 | > GVLはブロッキングするようなC関数(典型的には各OSのSleep()。write()とかも?)呼び出し中には解放される。というかCで書かれたライブラリ中でそういう風に作ってる。なのでその際他のThreadクラスのインスタンスも動作できる。この時だけはマルチコアが同時に動く(可能性がある)。
367 |
368 | > C拡張は基本的にGVLがかかった状態で呼び出される。これをしないと、C拡張を常に注意深くスレッドセーフにする必要があるし、そういうことをすべてのC拡張開発者に求めるのはキツかろう、という判断みたい。
369 |
370 | > ただ、C拡張の中でブロッキング関数を呼び出したり、純粋な重い処理をやるときにGVLを解放してやることはできる。その為にrb_thread_blocking_region()というのが用意されている。よってC拡張の中でrb_thread_blocking_region()が使われている場合はマルチコアが同時に動く(可能性がある)。
371 |
372 | > GVLはCRubyの実装を単純に保ち、かつシングルスレッド性能を落とさないためには今ん所まぁしょうがないよね~。forkでも使えば~。っていうところらしい。
373 |
374 | cf. http://d.hatena.ne.jp/kyab/20140215/1392485665
375 |
376 | > で、そこまで調べて気になったのが、じゃぁ例えば2つのスレッドが(内部でGVLを開放するようなメソッドを一切呼ばないで)ひたすらループしてたらどうなんの?ってこと。片方がGVLをロックしっぱなしだと、もう一方のスレッドは動作するチャンスがなくなっちゃうのでは?
377 |
378 | > 結論からいうと、そんなケースでも時々スレッドは切り替わる。以下の凄まじく長いGVL(=GIL)に関する記事によると、タイマースレッドというのが裏で動いていて、時々フラグを立てるらしい。タイマースレッドはCレベルで書かれていて、もちろんGVLと関係なく動く。
379 |
380 | 要約すると ruby は、
381 |
382 | * GVLがあるため結局マルチコアを使った並列処理ができない
383 | * GVLを掴んでいるスレッドのみがCPUを使う事ができる
384 |
385 | と言っている。また、GVL が解放されるタイミングについては
386 |
387 | 1. ブロックするようなC関数(write とか)を呼び出したとき
388 | 2. タイマースレッドにより定期的な解放
389 |
390 | の2つがあると言っている。
391 |
392 | 図に書くと下図のようになる。Thread 1 でブロックするような処理を呼び出して待っている隙に、Thread 2 が実行されたり、
393 | タイマースレッドが定期的にスレッドの切り替えを行う。
394 |
395 | ```
396 | blocking I/O by timer thread
397 | | |
398 | v v
399 | +–––––––––––+–––––––––––+––––––––––+––––––––––+
400 | | | | | |
401 | CPU1 | Thread 1 | Thread 2 | Thread 1 | Thread 2 |
402 | | | | | |
403 | +–––––––––––+–––––––––––+––––––––––+––––––––––+
404 |
405 | +–––––––––––––––––––––––––––––––––––––––––––––+
406 | CPU2 | |
407 | | |
408 | +–––––––––––––––––––––––––––––––––––––––––––––+
409 | ```
410 |
411 | また、`rb_thread_blocking_region()` を使ってGVLを明示的に解放することができる。
412 | もとい、ruby 組み込みのブロックしそうなメソッドはそれを使って、GVLを解放するように実装してあるため、
413 | GVLが解放されるのである。
414 |
415 | なお、cool.io では libev にパッチをあてて、IOでブロックする場合に、GVLを解放するようにしているようだ。
416 |
417 | 参考文献
418 |
419 | * [class Thread](http://docs.ruby-lang.org/ja/1.9.3/class/Thread.html)
420 | * [覚え書き: SevenZipRuby 作成メモ 3 - マルチスレッド](http://masamitsu-murase.blogspot.com/2013/12/sevenzipruby-3.html)
421 | * [CRubyのGVLとビジーループ - kyabの日記](http://d.hatena.ne.jp/kyab/20140215/1392485665)
422 | * [Ruby Under a Microscope - Pat Shaughnessy](http://patshaughnessy.net/ruby-under-a-microscope)
423 |
424 | ## libev のサンプル
425 |
426 | cf. http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod
427 |
428 | ```c
429 | #include
430 | #include // for puts
431 |
432 | // every watcher type has its own typedef'd struct
433 | // with the name ev_TYPE
434 | ev_io stdin_watcher;
435 | ev_timer timeout_watcher;
436 |
437 | // all watcher callbacks have a similar signature
438 | // this callback is called when data is readable on stdin
439 | static void
440 | stdin_cb (EV_P_ ev_io *w, int revents)
441 | {
442 | puts ("stdin ready");
443 | // for one-shot events, one must manually stop the watcher
444 | // with its corresponding stop function.
445 | ev_io_stop (EV_A_ w);
446 |
447 | // this causes all nested ev_run's to stop iterating
448 | ev_break (EV_A_ EVBREAK_ALL);
449 | }
450 |
451 | // another callback, this time for a time-out
452 | static void
453 | timeout_cb (EV_P_ ev_timer *w, int revents)
454 | {
455 | puts ("timeout");
456 | // this causes the innermost ev_run to stop iterating
457 | ev_break (EV_A_ EVBREAK_ONE);
458 | }
459 |
460 | int
461 | main (void)
462 | {
463 | // use the default event loop unless you have special needs
464 | struct ev_loop *loop = EV_DEFAULT;
465 |
466 | // initialise an io watcher, then start it
467 | // this one will watch for stdin to become readable
468 | ev_io_init (&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ);
469 | ev_io_start (loop, &stdin_watcher);
470 |
471 | // initialise a timer watcher, then start it
472 | // simple non-repeating 5.5 second timeout
473 | ev_timer_init (&timeout_watcher, timeout_cb, 5.5, 0.);
474 | ev_timer_start (loop, &timeout_watcher);
475 |
476 | // now wait for events to arrive
477 | ev_run (loop, 0);
478 |
479 | // break was called, so exit
480 | return 0;
481 | }
482 | ```
483 |
484 | ## Cool.io のサンプル
485 |
486 | 先ほどの libev のサンプルを Cool.io インターフェースで書くとこんなかんじか
487 |
488 | ```ruby
489 | INTERVAL = 5,5
490 |
491 | class MyIOWatcher < Cool.io::IOWatcher
492 | attr_accessor :readable
493 | def on_readable
494 | self.readable = true
495 | end
496 | end
497 |
498 | def run
499 | reactor = Cool.io::Loop.new
500 |
501 | sw = MyIOWatcher.new(STDIN)
502 | sw.attach(reactor)
503 |
504 | tw = Cool.io::TimerWatcher.new(INTERVAL, true)
505 | tw.on_timer do
506 | reactor.stop if sw.readable
507 | end
508 | tw.attach(reactor)
509 |
510 | reactor.run
511 | # 内部でやっているのは以下のような処理
512 | # while reactor.running and reactor.has_active_watchers?
513 | # reactor.run_once # イベントが起こるまでブロック
514 | # end
515 |
516 | tw.detach
517 | sw.detach
518 |
519 | sw
520 | end
521 |
522 | run
523 | ```
524 |
525 | ## ノンブロッキング待ち受け vs ブロッキング待ち受け
526 |
527 | さきほどの例ではブロッキングで待ち受けしていたが、ノンブロッキングでの待ち受けを考えてみる。例えば以下のようなコード。
528 |
529 | ```ruby
530 | while reactor.running and reactor.has_active_watchers?
531 | reactor.run_nonblock
532 | end
533 | ```
534 |
535 | これはよくない。なぜならビジーループとなるからだ。CPU100%の占有率となるからだ。
536 | ではビジーループを避けるために sleep を入れてみたらどうだろうか?
537 |
538 | ```ruby
539 | while reactor.running and reactor.has_active_watchers?
540 | reactor.run_nonblock
541 | sleep 0.5
542 | end
543 | ```
544 |
545 | これもよくない。なぜなら 0.5 秒眠っている間はイベントを受け付けることができず、
546 | スループトッが落ちるからだ。
547 |
548 | 「イベント駆動」なので、ブロッキングでイベントを待ち受けて、
549 | イベントが届いたら発火する、のがあるべき姿である。
550 | 発火したあとの処理はできればノンブロッキングですぐに返って来て、
551 | ブロッキングのイベント待ち受け処理に入れると良い。
552 |
553 | ### ブロッキング待ち受けは終了させるのが難しい
554 |
555 | しかし、ブロッキング待ち受けには終了させるのが難しいという問題がある。
556 | 次のコードを考える。これは Cool.io::Loop#run メソッドと同等である。
557 |
558 | ```ruby
559 | while reactor.running and reactor.has_active_watchers?
560 | reactor.run_once
561 | end
562 | ```
563 |
564 | このループを終了させるには reactor.running フラグを false にしたのち、
565 | ブロッキング処理を終了させるためになんらかのゴミイベントを送信しなければならない。
566 | 忘れずにイベントを送信しなければならない。
567 |
568 | ### タイムアウト付きブロッキング待ち受け
569 |
570 | 指定時間がすぎたら `#run_once` メソッドを抜ける timeout オプションの実装を考える。
571 |
572 | ```ruby
573 | timeout = 0.5
574 | while reactor.running and reactor.has_active_watchers?
575 | reactor.run_once(timeout)
576 | end
577 | ```
578 |
579 | これならば、ノンブロッキングの場合と異なり、
580 | 0.5 sec の間に 100 回イベントが来ても受け付けられスループットも落ちないし、
581 | 最後にイベントを投げなくてもループを終了することができる。
582 |
583 | ## Fluentd でみる Cool.io の実例
584 |
585 | では、Fluentd の in_forward プラグインで実際どのように Cool.io を利用しているのか見てみよう。
586 |
587 | [lib/fluent/plugin/in_forward.rb#L39-L53](https://github.com/fluent/fluentd/blob/ba5602f9e86540ebb3dcd2c8abc38ceb20ebf7cd/lib/fluent/plugin/in_forward.rb#L39-L53)
588 |
589 | UDP で送られて来る heartbeat 待ち受け watcher および、
590 | Coolio::TCPServer を使った TCP でのデータ送信待ち受け watcher を
591 | Coolio::Loop に attach している。
592 |
593 | どちらの watcher のいずれのイベントが来ても発火できるようにすべてのイベントを同時にブロッキング状態で待ち受ける。
594 |
595 | メインスレッドでブロッキング待ち受けしてしまうと他の処理(他のプラグインの処理など)がなにもできなくなってしまうので、
596 | `Thread.new(&method(:run))` して別スレッドで待ち受ける。
597 |
598 | Ruby には GVL があるが、ブロッキングしているスレッドではGVLが解放されるので、
599 | メインスレッドのほうで処理を続行できる。
600 |
601 | ```ruby
602 | def start
603 | @loop = Coolio::Loop.new
604 |
605 | @lsock = listen
606 | @loop.attach(@lsock)
607 |
608 | @usock = SocketUtil.create_udp_socket(@bind)
609 | @usock.bind(@bind, @port)
610 | @usock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
611 | @hbr = HeartbeatRequestHandler.new(@usock, method(:on_heartbeat_request))
612 | @loop.attach(@hbr)
613 |
614 | @thread = Thread.new(&method(:run))
615 | @cached_unpacker = $use_msgpack_5 ? nil : MessagePack::Unpacker.new
616 | end
617 | ```
618 |
619 | ```ruby
620 | def listen
621 | log.info "listening fluent socket on #{@bind}:#{@port}"
622 | s = Coolio::TCPServer.new(@bind, @port, Handler, @linger_timeout, log, method(:on_message))
623 | s.listen(@backlog) unless @backlog.nil?
624 | s
625 | end
626 | ```
627 |
628 | ```ruby
629 | def run
630 | @loop.run
631 | rescue => e
632 | log.error "unexpected error", :error => e, :error_class => e.class
633 | log.error_backtrace
634 | end
635 | ```
636 |
637 | [lib/fluent/plugin/in_forward.rb#L55-L65](https://github.com/fluent/fluentd/blob/ba5602f9e86540ebb3dcd2c8abc38ceb20ebf7cd/lib/fluent/plugin/in_forward.rb#L55-L65)
638 |
639 | 全ての watchers を detach し、Coolio::Loop#stop (Coolio::Loop#run 内の running 変数を false にする)している。
640 |
641 | Coolio::Loop#run 内のブロッキング待ち受けを抜けさせるために
642 | `TCPSocket.open` で接続して on_connect イベントを送信することで
643 | run メソッドを終了させている。
644 |
645 | ```ruby
646 | def shutdown
647 | @loop.watchers.each {|w| w.detach }
648 | @loop.stop
649 | @usock.close
650 | listen_address = (@bind == '0.0.0.0' ? '127.0.0.1' : @bind)
651 | # This line is for connecting listen socket to stop the event loop.
652 | # We should use more better approach, e.g. using pipe, fixing cool.io with timeout, etc.
653 | TCPSocket.open(listen_address, @port) {|sock| } # FIXME @thread.join blocks without this line
654 | @thread.join
655 | @lsock.close
656 | end
657 | ```
658 |
659 | ### おまけ:抱えている問題
660 |
661 | `TCPSocket.open` で接続することによって、`Coolio::Loop#run` を抜けさせているのだが、
662 | この TCPSocket.open が失敗することがある。より詳細には Socket#pack_sockaddr_in で固まることがある。
663 | このため、Fluentd が終了せずに固まることがあった。
664 |
665 | * [Bug #9525: Stuck with Socket.pack_sockaddr_in - ruby-trunk - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/9525)
666 | * [Fluentd が終了しないことがある - togetter](http://togetter.com/li/572080)
667 | * [Fluentd が終了しないことがある - その2 - togetter](http://togetter.com/li/622823)
668 |
669 | そこで、`#run_once` に timeout オプションを実装したパッチがこちらである。=>
670 | [[WIP] timeout option for Coolio::Loop#run_once by sonots · Pull Request #29 · tarcieri/cool.io](https://github.com/tarcieri/cool.io/pull/29)
671 |
672 | # Cool.io の構造
673 |
674 | ## ファイル構造
675 |
676 | ```bash
677 | .
678 | ├── CHANGES.md
679 | ├── Gemfile
680 | ├── Gemfile.lock
681 | ├── LICENSE
682 | ├── README.md
683 | ├── Rakefile
684 | ├── cool.io.gemspec
685 | ├── examples
686 | │ ├── dslified_echo_client.rb
687 | │ ├── dslified_echo_server.rb
688 | │ ├── echo_client.rb
689 | │ ├── echo_server.rb
690 | │ ├── google.rb
691 | │ └── httpclient.rb
692 | ├── ext # C拡張コードのディレクトリ
693 | │ ├── cool.io
694 | │ │ ├── cool.io.h # ヘッダ
695 | │ │ ├── cool.io_ext.c # cool.io_ext.so を作る大元
696 | │ │ ├── ev_wrap.h # win 用 define を追加した ev.h のラッパ. こっちを include する
697 | │ │ ├── extconf.rb # configures Makefile
698 | │ │ ├── iowatcher.c # Coolio::IOWatcher。Ruby IO の readability, writability イベント
699 | │ │ ├── libev.c # ev_wrap.h を使うようにした ev.c のラッパ
700 | │ │ ├── loop.c # Coolio::Loop。ループを司るクラス。WatcherをLoopにattachして使う
701 | │ │ ├── stat_watcher.c # Coolio::StatWatcher。ファイルstatイベント(mtime, ino, etc)
702 | │ │ ├── timer_watcher.c # Coolio::TimerWatcher。時間イベント
703 | │ │ ├── utils.c # Coolio::Utils。ncpus (CPUの数), maxfds (最大 fd 数) の取得ができる
704 | │ │ ├── watcher.c # Coolio::Watcher。Watcherクラス群のbase
705 | │ │ └── watcher.h # Coolio::Watcher 用のヘッダ、ではなくて Watcher マクロ関数の実装
706 | │ ├── http11_client # 非同期httpクライアント.
707 | │ │ ├── LICENSE
708 | │ │ ├── ext_help.h
709 | │ │ ├── extconf.rb
710 | │ │ ├── http11_client.c
711 | │ │ ├── http11_parser.c
712 | │ │ ├── http11_parser.h
713 | │ │ └── http11_parser.rl
714 | │ ├── iobuffer # non-blocking プログラム用 I/O バッファライブラリ
715 | │ │ ├── extconf.rb
716 | │ │ └── iobuffer.c
717 | │ └── libev # libev に ruby_gil.patch をあてたもの
718 | │ ├── Changes
719 | │ ├── LICENSE
720 | │ ├── README
721 | │ ├── README.embed
722 | │ ├── ev.c
723 | │ ├── ev.h
724 | │ ├── ev_epoll.c
725 | │ ├── ev_kqueue.c
726 | │ ├── ev_poll.c
727 | │ ├── ev_port.c
728 | │ ├── ev_select.c
729 | │ ├── ev_vars.h
730 | │ ├── ev_win32.c
731 | │ ├── ev_wrap.h
732 | │ ├── ruby_gil.patch
733 | │ └── test_libev_win32.c
734 | ├── lib # rubyコードのディレクトリ
735 | │ ├── cool.io
736 | │ │ ├── async_watcher.rb # 別スレッドの Loop にパイプ経由でイベントを送る
737 | │ │ ├── custom_require.rb # 同梱c拡張をrequireするためにごにょごにょしている
738 | │ │ ├── dns_resolver.rb # non-blocking DNS resolver. これも IOWatcher で解決したら on_success が呼ばれる
739 | │ │ ├── dsl.rb # http://coolio.github.io で解説しているDSL定義。Fluentdでは全く使っていない
740 | │ │ ├── eventmachine.rb # coolio を使った eventmachine api の実装。実験的
741 | │ │ ├── http_client.rb # non-blocking http client. http11_client を利用. 接続成功したら on_connect が呼ばれるとか
742 | │ │ ├── io.rb # non-blocking IO. クライアントのベースクラス
743 | │ │ ├── iowatcher.rb # Coolio::IOWatcher の一部 ruby 実装
744 | │ │ ├── listener.rb # Coolio::Listener, TCPListener, UNIXListener
745 | │ │ ├── loop.rb # Coolio::Loop の一部 ruby 実装
746 | │ │ ├── meta.rb # watcher_delegate, event_callback クラスメソッド(マクロ)を生やす
747 | │ │ ├── server.rb # Coolio::Server, TCPServer, UNIXServer. Listener の処理に加えて接続があったときに connection (socket) オブジェクトを作る
748 | │ │ ├── socket.rb # Coolio::Socket, TCPSocket, UNIXSocket
749 | │ │ ├── timer_watcher.rb # Coolio::TImerWatcher の一部 ruby 実装
750 | │ │ └── version.rb
751 | │ ├── cool.io.rb # require する入り口
752 | │ ├── coolio.rb # require 'cool.io' してるだけ
753 | └── spec
754 | ├── async_watcher_spec.rb
755 | ├── dns_spec.rb
756 | ├── spec_helper.rb
757 | ├── stat_watcher_spec.rb
758 | ├── tcp_server_spec.rb
759 | ├── timer_watcher_spec.rb
760 | ├── unix_listener_spec.rb
761 | └── unix_server_spec.rb
762 | ```
763 |
764 | ## クラス構造
765 |
766 | ToDo: クラス図書く
767 |
768 | ```ruby
769 | Loop
770 | Watcher
771 | StatWatcher < Watcher
772 | IOWatcher (extend Meta) < Watcher
773 | TimerWatcher (extend Meta) < Watcher
774 | AsyncWatcher < IOWatcher < Watcher
775 | DNSResolver < IOWatcher < Watcher
776 | Listener < IOWatcher < Watcher
777 | TCPListener < Listener < IOWatcher < Watcher
778 | UNIXListenrer < Listener < IOWatcher < Watcher
779 | Server < Listener < IOWatcher < Watcher
780 | TCPServer < Server < Listener < IOWatcher < Watcher
781 | UNIXServer < Server < Listener < IOWatcher < Watcher
782 | IO (extend Meta)
783 | Socket < IO
784 | TCPSocket < Socket < IO
785 | UNIXSocket < Socket < IO
786 | HTTPClient < TCPSocket < Socket < IO
787 | ```
788 |
789 | ## イベント発火の流れ
790 |
791 | [lib/cool.io/loop.rb#L91-L99](https://github.com/tarcieri/cool.io/blob/64657d653cafe2ed1c02f4ba38c347e6b9faf2b9/lib/cool.io/loop.rb#L91-L99)
792 |
793 | アプリ側で Coolio::Loop.new して、run メソッドを呼ぶと、run_once のループに入る。
794 |
795 | ```ruby
796 | def run(timeout = nil)
797 | raise RuntimeError, "no watchers for this loop" if @watchers.empty?
798 |
799 | @running = true
800 | while @running and not @active_watchers.zero?
801 | run_once(timeout)
802 | end
803 | @running = false
804 | end
805 | ```
806 |
807 | [ext/cool.io/loop.c#L183-L220](https://github.com/tarcieri/cool.io/blob/64657d653cafe2ed1c02f4ba38c347e6b9faf2b9/ext/cool.io/loop.c#L183-L220)
808 |
809 | run_once の中では自分が実装した timeout オプションのためにちょっとごにょごにょやっているが、
810 | 基本的には RUN_LOOP メソッドを呼んでイベントが来るまでブロックし、
811 | イベントが来たら Coolio_Loop_dispatch_events を呼んで dispatch している。
812 |
813 | EVLOOP_ONESHOT なので1度イベントを受け取ったら loop は終了する。
814 |
815 | ```c
816 | static VALUE Coolio_Loop_run_once(int argc, VALUE *argv, VALUE self)
817 | {
818 | VALUE timeout;
819 | VALUE nevents;
820 | struct Coolio_Loop *loop_data;
821 |
822 | rb_scan_args(argc, argv, "01", &timeout);
823 |
824 | if (timeout != Qnil && NUM2DBL(timeout) < 0) {
825 | rb_raise(rb_eArgError, "time interval must be positive");
826 | }
827 |
828 | Data_Get_Struct(self, struct Coolio_Loop, loop_data);
829 |
830 | assert(loop_data->ev_loop && !loop_data->events_received);
831 |
832 | /* Implement the optional timeout (if any) as a ev_timer */
833 | /* Using the technique written at
834 | http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#code_ev_timer_code_relative_and_opti,
835 | the timer is not stopped/started everytime when timeout is specified, instead,
836 | the timer is stopped when timeout is not specified. */
837 | if (timeout != Qnil) {
838 | /* It seems libev is not a fan of timers being zero, so fudge a little */
839 | loop_data->timer.repeat = NUM2DBL(timeout) + 0.0001;
840 | ev_timer_again(loop_data->ev_loop, &loop_data->timer);
841 | } else {
842 | ev_timer_stop(loop_data->ev_loop, &loop_data->timer);
843 | }
844 |
845 | /* libev is patched to release the GIL when it makes its system call */
846 | RUN_LOOP(loop_data, EVLOOP_ONESHOT);
847 |
848 | Coolio_Loop_dispatch_events(loop_data);
849 | nevents = INT2NUM(loop_data->events_received);
850 | loop_data->events_received = 0;
851 |
852 | return nevents;
853 | }
854 | ```
855 |
856 | [ext/cool.io/loop.c#L30-L33](https://github.com/tarcieri/cool.io/blob/64657d653cafe2ed1c02f4ba38c347e6b9faf2b9/ext/cool.io/loop.c#L30-L33)
857 |
858 | なお、RUN_LOOP はフラグの on/off をしているだけで、実質的には libev の ev_loop 関数
859 |
860 | ```c
861 | #define RUN_LOOP(loop_data, options) \
862 | loop_data->running = 1; \
863 | ev_loop(loop_data->ev_loop, options); \
864 | loop_data->running = 0;
865 | ```
866 |
867 | [ext/cool.io/loop.c#L246-L261](https://github.com/tarcieri/cool.io/blob/64657d653cafe2ed1c02f4ba38c347e6b9faf2b9/ext/cool.io/loop.c#L246-L261)
868 |
869 | 受け取ったイベントの数だけ、watcher_data (Coolio::Watcher オブジェクト) の dispatch_callback を呼ぶ
870 |
871 | ```c
872 | static void Coolio_Loop_dispatch_events(struct Coolio_Loop *loop_data)
873 | {
874 | int i;
875 | struct Coolio_Watcher *watcher_data;
876 |
877 | for(i = 0; i < loop_data->events_received; i++) {
878 | /* A watcher with pending events may have been detached from the loop
879 | * during the dispatch process. If so, the watcher clears the pending
880 | * events, so skip over them */
881 | if(loop_data->eventbuf[i].watcher == Qnil)
882 | continue;
883 |
884 | Data_Get_Struct(loop_data->eventbuf[i].watcher, struct Coolio_Watcher, watcher_data);
885 | watcher_data->dispatch_callback(loop_data->eventbuf[i].watcher, loop_data->eventbuf[i].revents);
886 | }
887 | }
888 | ```
889 |
890 | [ext/cool.io/iowatcher.c#L181-L189](https://github.com/tarcieri/cool.io/blob/64657d653cafe2ed1c02f4ba38c347e6b9faf2b9/ext/cool.io/iowatcher.c#L181-L189)
891 |
892 | Coolio::Watcher 自体は dispatch_callback を定義していないので、子クラスである IOWatcher クラスを見てみる。
893 | IOWatcher は Coolio::TCPServer, Coolio::TCPSocket などの基底クラスとなるのでおそらく一番お世話になるもの。
894 |
895 | EV_READ イベントならば、on_readable メソッドを、EV_WRITE ならば on_writable メソッドを呼んでいる。
896 | ruby レベルでの継承をサポートできるように rb_funcall を使って呼び出している。
897 |
898 | ```
899 | static void Coolio_IOWatcher_dispatch_callback(VALUE self, int revents)
900 | {
901 | if(revents & EV_READ)
902 | rb_funcall(self, rb_intern("on_readable"), 0, 0);
903 | else if(revents & EV_WRITE)
904 | rb_funcall(self, rb_intern("on_writable"), 0, 0);
905 | else
906 | rb_raise(rb_eRuntimeError, "unknown revents value for ev_io: %d", revents);
907 | }
908 | ```
909 |
910 | ## イベント受信の流れ
911 |
912 | たとえば IOWatcher の場合に、どのイベントを受け取るのか
913 |
914 | [ext/cool.io/iowatcher.c#L58-L94](https://github.com/tarcieri/cool.io/blob/64657d653cafe2ed1c02f4ba38c347e6b9faf2b9/ext/cool.io/iowatcher.c#L58-L94)
915 |
916 | ここで libev で定義されている EV_READ | EV_WRITE イベントを受け取ったら、
917 | Coolio_IOWatcher_libev_callback を呼ぶように、ev_io_init を呼び出して登録している。
918 |
919 | 登録された状態で、ev_loop が呼び出されるとイベントを受け取るまでブロックする仕組み。
920 |
921 | ```c
922 | static VALUE Coolio_IOWatcher_initialize(int argc, VALUE *argv, VALUE self)
923 | {
924 | VALUE io, flags;
925 | char *flags_str;
926 | int events;
927 | struct Coolio_Watcher *watcher_data;
928 | #if HAVE_RB_IO_T
929 | rb_io_t *fptr;
930 | #else
931 | OpenFile *fptr;
932 | #endif
933 |
934 | rb_scan_args(argc, argv, "11", &io, &flags);
935 |
936 | if(flags != Qnil)
937 | flags_str = RSTRING_PTR(rb_String(flags));
938 | else
939 | flags_str = "r";
940 |
941 | if(!strcmp(flags_str, "r"))
942 | events = EV_READ;
943 | else if(!strcmp(flags_str, "w"))
944 | events = EV_WRITE;
945 | else if(!strcmp(flags_str, "rw"))
946 | events = EV_READ | EV_WRITE;
947 | else
948 | rb_raise(rb_eArgError, "invalid event type: '%s' (must be 'r', 'w', or 'rw')", flags_str);
949 |
950 | Data_Get_Struct(self, struct Coolio_Watcher, watcher_data);
951 | GetOpenFile(rb_convert_type(io, T_FILE, "IO", "to_io"), fptr);
952 |
953 | watcher_data->dispatch_callback = Coolio_IOWatcher_dispatch_callback;
954 | ev_io_init(&watcher_data->event_types.ev_io, Coolio_IOWatcher_libev_callback, FPTR_TO_FD(fptr), events);
955 | watcher_data->event_types.ev_io.data = (void *)self;
956 |
957 | return Qnil;
958 | }
959 | ```
960 |
961 | [ext/cool.io/iowatcher.c#L175-L178](https://github.com/tarcieri/cool.io/blob/64657d653cafe2ed1c02f4ba38c347e6b9faf2b9/ext/cool.io/iowatcher.c#L175-L178)
962 |
963 | イベントを受け取ると、libev に登録した callback が呼ばれて、`Coolio::Loop#process_event` を呼び出す。
964 |
965 | ```c
966 | static void Coolio_IOWatcher_libev_callback(struct ev_loop *ev_loop, struct ev_io *io, int revents)
967 | {
968 | Coolio_Loop_process_event((VALUE)io->data, revents);
969 | }
970 | ```
971 |
972 | [ext/cool.io/loop.c#L102-L168](https://github.com/tarcieri/cool.io/blob/64657d653cafe2ed1c02f4ba38c347e6b9faf2b9/ext/cool.io/loop.c#L102-L168)
973 |
974 | 構造体に受け取ったイベントを登録し、events_received++ している。
975 |
976 | ```
977 | void Coolio_Loop_process_event(VALUE watcher, int revents)
978 | {
979 | struct Coolio_Loop *loop_data;
980 | struct Coolio_Watcher *watcher_data;
981 |
982 | /* The Global VM lock isn't held right now, but hopefully
983 | * we can still do this safely */
984 | Data_Get_Struct(watcher, struct Coolio_Watcher, watcher_data);
985 | Data_Get_Struct(watcher_data->loop, struct Coolio_Loop, loop_data);
986 |
987 | /* Well, what better place to explain how this all works than
988 | * where the most wonky and convoluted stuff is going on!
989 | *
990 | * Our call path up to here looks a little something like:
991 | *
992 | * -> release GVL -> event syscall -> libev callback
993 | * (GVL = Global VM Lock) ^^^ You are here
994 | *
995 | * We released the GVL in the Coolio_Loop_run_once() function
996 | * so other Ruby threads can run while we make a blocking
997 | * system call (one of epoll, kqueue, port, poll, or select,
998 | * depending on the platform).
999 | *
1000 | * More specifically, this is a libev callback abstraction
1001 | * called from a real libev callback in every watcher,
1002 | * hence this function not being static. The real libev
1003 | * callbacks are event-specific and handled in a watcher.
1004 | *
1005 | * For syscalls like epoll and kqueue, the kernel tells libev
1006 | * a pointer (to a structure with a pointer) to the watcher
1007 | * object. No data structure lookups are required at all
1008 | * (beyond structs), it's smooth O(1) sailing the entire way.
1009 | * Then libev calls out to the watcher's callback, which
1010 | * calls this function.
1011 | *
1012 | * Now, you may be curious: if the watcher already knew what
1013 | * event fired, why the hell is it telling the loop? Why
1014 | * doesn't it just rb_funcall() the appropriate callback?
1015 | *
1016 | * Well, the problem is the Global VM Lock isn't held right
1017 | * now, so we can't rb_funcall() anything. In order to get
1018 | * it back we have to:
1019 | *
1020 | * stash event and return -> acquire GVL -> dispatch to Ruby
1021 | *
1022 | * Which is kinda ugly and confusing, but still gives us
1023 | * an O(1) event loop whose heart is in the kernel itself. w00t!
1024 | *
1025 | * So, stash the event in the loop's data struct. When we return
1026 | * the ev_loop() call being made in the Coolio_Loop_run_once_blocking()
1027 | * function below will also return, at which point the GVL is
1028 | * reacquired and we can call out to Ruby */
1029 |
1030 | /* Grow the event buffer if it's too small */
1031 | if(loop_data->events_received >= loop_data->eventbuf_size) {
1032 | loop_data->eventbuf_size *= 2;
1033 | loop_data->eventbuf = (struct Coolio_Event *)xrealloc(
1034 | loop_data->eventbuf,
1035 | sizeof(struct Coolio_Event) * loop_data->eventbuf_size
1036 | );
1037 | }
1038 |
1039 | loop_data->eventbuf[loop_data->events_received].watcher = watcher;
1040 | loop_data->eventbuf[loop_data->events_received].revents = revents;
1041 |
1042 | loop_data->events_received++;
1043 | }
1044 | ```
1045 |
1046 | この callback が終わると、`#run_once` の RUN_LOOP を抜けて
1047 | `Coolio_Loop_dispatch_events` が呼び出され、events_received の数だけ、on_readable や on_writable が呼び出される、ということになる。
1048 |
1049 | なので、libev の callback の仕組みをそのまま使って直接 on_readable などを呼び出しているわけではなく、
1050 | 別途 cool.io 側の実装で on_readable などを呼び出している。
1051 |
1052 | どうしてそうしているのかは、Coolio_Loop_process_event のコメントに書いてあるが、GVL がらみであるようだ。
1053 | IOWatcher を new したメインスレッドで GVL を保持したくない、といった所か??
1054 |
1055 | # まとめ
1056 |
1057 | Cool.io に入門した。Cool.io に入門するための前準備として
1058 |
1059 | * Ruby C拡張
1060 | * GVL
1061 | * libev
1062 | * ノンブロッキング待ち受けとブロッキング待ち受け
1063 |
1064 | の解説をした。その後、
1065 |
1066 | * Cool.io の構造
1067 | * イベント発火の流れ
1068 | * イベント受信の流れ
1069 |
1070 | を解説した。
1071 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fluend-scr
2 |
3 | 1. [Fluentd の out_forward と heartbeat](./01_out_forward_heartbeat.md)
4 | 2. [Fluentd の out_forward と BufferedOutput](./02_out_forward_buffered.md)
5 | 3. [Fluentd の in_forward とプラグイン呼び出し](./03_in_forward_plugin.md)
6 | 4. [fluent-agent-lite](./04_fluent_agent_lite.md)
7 |
8 | # 前準備
9 |
10 | https://github.com/fluent/fluentd を clone して動かせるようにしておこう ^^
11 |
12 | ruby は入っているものとして、
13 |
14 | ```
15 | $ git clone git@github.com:fluent/fluentd.git
16 | $ cd fluentd
17 | $ bundle
18 | $ bundle exec fluentd -c fluent.conf # 動いてるっぽければとりあえずOK
19 | ```
20 |
21 |
--------------------------------------------------------------------------------
/toc.rb:
--------------------------------------------------------------------------------
1 | require 'uri'
2 | filename = ARGV.first
3 | File.open(filename) do |file|
4 | while line = file.gets
5 | if /^(#+) (.*)/ =~ line
6 | next if $1.size >= 3
7 | level = $1.size - 1
8 | headline = $2
9 | print " " * 2 * level
10 | puts "* [#{headline}](##{URI.escape(headline.downcase.tr(' ', '-').gsub('.', ''))})"
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------