├── 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 | --------------------------------------------------------------------------------