ノンブロッキングI/Oがよく解っていなかったので作りながら調べてみた

モチベーション

WEBエンジニアとして活動してきてますが、技術的な棚卸しとして、普段良く使っているツールがどういったものかを少し深掘りしてみようという活動の一環。

nginx軽くて早くてお世話になっていますが、どうして軽くて早いのかよく解っておらず、ノンブロッキングI/Oという言葉もふわっと流していたので、深掘りしてみようという感じです。

ざっと色々なブログなどを見てみたのですが、いまいち消化できずにいたので、せっかくなので少し手を動かしながらやるか!ということで、少し勉強中のgolangで書いてみた。

続きを読む

Deep Workを読んでみて

かなり良かったので感想をまとめてみる

日本語版もあるけど、英語の勉強も兼ねて英語版を読んでみた。

単語解らなすぎてきつい時もあったけど、後半ぐらいまで進むとコンテキストがだいぶ解ってくるので割とスラスラ読めて良かった(それでも日本語の5倍ぐらいは時間かかってそう)

技術系のドキュメントを読むのがだいぶ楽に感じたり、とりあえず読破した達成感があったりしたのは良かったところ。

本の概要としては、

最近のインターネット社会では、Deep Work(深い集中力がいる仕事、難しいけど価値があるような)の重要性が上がってきているけど、一方で Deep Work すること自体は昔に比べて難しい環境になってきてしまっている。

そういった環境自体が良いか悪いかの議論は置いておいて、こういったシチュエーションで Deep Work するスキルを身につけて、良いアウトプットを出すことができるようになれば、色々と得ができるかも?

Deep Workの価値的な話

何かの物事に深く集中すること自体に、人間は価値を見出すことができる(craftsmanship)という話をしていて、

この辺りが個人的にとても興味深かったのですが、今の時代は解りやすい信仰的なものが薄れてきて、どういった物事に価値があって輝かしいことかが解りにくくなっている世界(post Enlightment world)でニヒリズム虚無主義)になりやすい世界と表現されいて、

自分としてもこういった言葉は全然知らなかったんですが、感覚的にはかなり理解できるなあという感じで、

価値を生み出すというよりも、既にそこにある価値を見出すスキルを耕していくことが、ニヒリズム的な価値観から解放されるやりかななのでは、 -> Deep Work につながる。

Deep Work する難しさについて。

人間は常にちょっとした衝動、欲求を感じている(何か食べたい、寝たい、ネットみたい、テレビ見たいなど)

こういった衝動は過小評価されがちで、思っているよりも強いもので、これらは Deep Work の敵。

また、日常的にこういった衝動に侵されている状態では、深い集中をすること自体が無理になってしまっている。(ほんのちょっとした空き時間などですぐスマホを見てしまったりするのが日常にってしまっている状態)

これはマルチタスクは難しいよね、という話と同じで、人間の脳のタスクの切り替えは簡単ではなく、前にやっていた作業などに注意が奪われてしまい、1つのことに集中することができなくなってしまう。

また、こういった性質を理解した上で、 Deep Work の重要性を理解していれば大丈夫でしょ?という訳でもなく、

こういった衝動に争う意思力というのは有限で、消費されていってしまうものらしい(筋肉が疲れるように)

じゃあどうするか。

習慣や儀式化することで、意思力に頼らずに Deep Work しよう。色々な偉人はこういったルーティーンを取り入れていた。

色々な習慣化、儀式化の方法があるよ。


Deep Work の対象としては、多くのことに取り組むより、少ない実質的な利益が解りやすいことの方が、定期的な熱量が生まれやすい


Deep Work の取り組みの評価としては、行っていること自体の評価は結構後になってしまうので、日々の取り組みに影響しない。

例えば 1週間で Deep Work できた時間を測ることで、日々の取り組みの評価ができ、どう改善していくかを考える指標とすることができる。


思ったこと

自分の場合だとSNSより会社のslackにだいぶ注意が奪われていたなと。これは何とかしないと。

あと人間の意思や脳はそんなに都合よくできていないので、色々と工夫、改善していく余地がかなりある。

(わりと自然に身を任せていけばいいのでは、という楽観的な感じだった)

Deep Work の中身にはそれほど触れられていないので、工夫していかないと(考え方自体のスキーマ、テンプレなど)

あと Deep Work することができても、個人でどれくらいの価値を生み出すことができるんだろうという弱気な気持ちも。

本で紹介されている例が偉人ばかりなので。

まあ今までできていなかったこと、意識できていなかったことがかなりあるので、トライしていきたいという感じ。

ActiveRecordへのattributeの代入時に何が起こっているか

ActiveRecordを継承したモデルへのattributeの代入時の挙動について。

通常の attribute

activerecord/attribute_methods/write.rb 内で #{attribute_name}= メソッドを動的に定義している。

その中で @attributes<ActiveRecord::AttributeSet> (ハッシュ的なもの)に対して attribute_nameのキーに値を設定している。

has manyとかの _ids への代入

active_record/associations/builder/collection_association.rb 内で、 #{attribute_name}_ids= メソッドを動的に定義していて、

その中で association(:#{name}).ids_writer(ids) を呼び出している

association ??

関連先のモデル自体ではなく、関連元、関連先、連携の設定の情報をもった、連携用の機能を色々もったオブジェクト(ActiveRecord::Associations::Association)

associationオブジェクトは、関連の種類(belongs_to, has_many ...)によって機能が拡張されている。


ids_writer内の処理では、idsからインスタンスリストを生成して、削除分の差分が delete_all され、追加分の差分が insert される

devise_for

Deviseを使うとき、 routes.rbに書く devise_for が何をやっているか。

devse_for?

devise/rails/routes.rb の devise_for のコメント欄のところ

# Includes devise_for method for routes. This method is responsible to
# generate all needed routes for devise, based on what modules you have
# defined in your model.

モデル上でDeviseのモジュール設定をベースとして、routesの設定をする。

また、

devise/mapping.rb の先頭のコメント。

# Responsible for handling devise mappings and routes configuration. Each
 # resource configured by devise_for in routes is actually creating a mapping
 # object. You can refer to devise_for in routes for usage options.

devise_for の定義は、実質はDevise::Mapping のインスタンスを作成している。

mappingオブジェクト作った後に、 devise_scope を呼び出して、 devise_#{module name} を呼び出すことで、routesを定義している。

# devise_for メソッドの最下部で、devise_scope を呼び出している箇所。
devise_scope mapping.name do
  with_devise_exclusive_scope mapping.fullpath, mapping.name, options do
    routes.each { |mod| send("devise_#{mod}", mapping, mapping.controllers) }
  end
end

例えば、passwordモジュールのroutesは下記のように定義されてる

def devise_password(mapping, controllers) #:nodoc:
  resource :password, only: [:new, :create, :edit, :update],
    path: mapping.path_names[:password], controller: controllers[:passwords]
end

mappingオブジェクトの設定を元に resource の設定をしてる。

devise_scope?

devise_scope :resource_name &block で、ブロックにroutesを定義する処理を指定できる。

devise_for でモジュール毎の基本的な設定と各種オプションでの調整はできる。

devise_scope はもっと自由にroutesをカスタマイズするときに使うとのこと。

constraints (配下のネストされたルートに制約をかける処理)で request.env["devise.mapping"] に Devise.mappings[scoep]を設定して、

scope :resource_name に、上記で定義したroute情報を渡している

constraints の処理は、initializeのときではなく、実際に対象のrouteにアクセスがあったときに走るので、

deviseのroutesのリクエスト毎に request.env["devise.mapping"] にMapping情報を設定している(目的は謎。)

まとめ

Railsのroutes周りの仕組みをハックして、deviseの各モジュールで必要なroutesの設定を、簡単で、柔軟な形で行えるようにしているイメージ。

Sidekiqの仕組み

Railsのジョブキューのgemで有名なSidekiq。 どのように動いているかよくわかっていないので調べてみた。

エンキュー

ActibeJob.perform_later

キューに入れたい処理はActiveJob::Baseを拡張したクラスのperformメソッドに実装し、

perform_laterを呼び出すと、エンキューの処理が走る。

Class SampleJob < ActiveJob::Base
  def perform
    p "Hello!"
  end
end

...

SampleJob.perform_later

queue_adapter.enqueue

perform_laterは ActiveJob.queue_adapter に設定されているオブジェクトの enqueue にjobインスタンスを投げます。 queue_adapterの設定はデフォルトでは、 :async(AsyncAdapter) で、configで以下のように指定すると、SidekiqAdapterが設定されます。

config.active_job.queue_adapter = :sidekiq

他にどんなアダプターがあるかは、activjob/queue_adapters.rb のコメントに結構細かく書かれています。

rails/queue_adapters.rb at master · rails/rails · GitHub

SidekiqAdapter.enqueue

def enqueue(job) #:nodoc:
  #Sidekiq::Client does not support symbols as keys
  job.provider_job_id = Sidekiq::Client.push \
    'class'   => JobWrapper,
    'wrapped' => job.class.to_s,
    'queue'   => job.queue_name,
    'args'    => [ job.serialize ]
end

こんな感じ。 ジョブのクラス名や引数を Sidekiq::Client に投げている。

class として指定されている。JobWrapper の実装はかなり薄くて、

class JobWrapper #:nodoc:
  include Sidekiq::Worker

  def perform(job_data)
    Base.execute job_data.merge('provider_job_id' => jid)
  end
end

これだけです。 Base.executeは受け取ったjob_dataをdeserializeして、performを実行します。

Sidekiq.client

Sidekiq.clientのpush処理では、受け取ったジョブ情報をRedis上に保存できる状態に成形し、ジョブIDの情報を追加して、 Redisの queues:#{queue_name} キーに追加します。

サンプルで追加してみた中身はこんな感じのjsonです。

{
    "class": "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
    "wrapped": "SampleJob",
    "queue": "default",
    "args": [
        {
            "job_class": "SampleJob",
            "job_id": "ae214f55-3d84-4037-8086-be111661ec6a",
            "queue_name": "default",
            "priority": null,
            "arguments": [],
            "locale": "ja"
        }
    ],
    "retry": true,
    "jid": "d1ecd3aa4bf5340b5c2a52f6",
    "created_at": 1506733202.404303,
    "enqueued_at": 1506733202.405045
}

ジョブの取得と実行

Redisにジョブを追加するだけではジョブは実行されません。 ジョブ情報を取得して、実行する必要があります。

取得

bundle exec sidekiq を実行することで、ジョブを取得、実行するプロセスを起動できます。

プロセスが起動すると、Sidekiq::Launcher.run が実行されます。

sidekiq/launcher.rb at master · mperham/sidekiq · GitHub

まず、Sidekiq::Managerがworkerを作成し、起動します。(デフォルトの設定では25個作成されます。)

workerはそれぞれのスレッドを起動し、ジョブ情報をRedisから取得しに行きます。

wokerの中身は Sidekiq::Processor で、Processor.get_one から下記の Sidekiq::BasicFetch.retrieve_work が呼び出され、Redisからジョブ情報を取得しています。

sidekiq/fetch.rb at master · mperham/sidekiq · GitHub

実行

Redisから取得したジョブ情報を元に、ジョブのクラスと引数情報を deserialize して、worker上のスレッドで実行されます

sidekiq/processor.rb at master · mperham/sidekiq · GitHub

performが呼び出されてます。

Railsのloadは元のプロセス起動時に行われています。

sidekiq/cli.rb at master · mperham/sidekiq · GitHub

scheduled

下記のように waitwait_untill を設定した場合はどうなるか?

SampleJob.set(wait: 5.minutes).perform_later

エンキュー

指定された値から、実行する時刻を計算し、 queue_adapterの enqueue_at にジョブ情報と一緒に渡されます。

#SidekiqAdapter.enqueue_at
def enqueue_at(job, timestamp) #:nodoc:
  job.provider_job_id = Sidekiq::Client.push \
    'class'   => JobWrapper,
    'wrapped' => job.class.to_s,
    'queue'   => job.queue_name,
    'args'    => [ job.serialize ],
    'at'      => timestamp
end

Sidekiq::Client側の処理

Sidekiq::Client側の処理では、 at が指定されていると sorted set に timestampでソートする形でジョブ情報が登録されます。 (上記の通常(at指定なし)のジョブ情報リストとは違うキーに保持されます)

取得、実行

Sidekiqのプロセスは上記で説明した プロセスを実行する woker を管理する manager の他に、 at 指定で登録されているジョブを ジョブキューに登録する poller が動いています。

pollerは5秒ごとに、 sorted set から 現在時刻より前 の timestamp のジョブ情報がないかを見に行き、 対象のジョブがあった場合は Sidekiq::Client::push を呼び出し、 sorted set から ジョブキューへジョブ情報を移します。

この処理により、 at で指定したジョブも worker達が処理する対象になってくれます。

retry

プロセスでエラーが起きた時、

retry が有効な場合で、retry数がまだMAX retry数内(デフォルトは25)の場合は、ジョブ情報が retry queue に追加されます。

retry queue に保持される情報は、通常のジョブ情報に、エラー情報とリトライ回数などの情報が加えらえれたものになります。

上記で話した scheduledのキューを処理する poller は、retryのキュー情報も見るようになっているので、retryに入れられた情報は、

自然とジョブキューへ追加されます。

リトライ実行のディレイはリトライ数が多くなるごとに指数関数的に多くなるようになっています。

ディレイ時間の計算処理 sidekiq/job_retry.rb at master · mperham/sidekiq · GitHub

リトライ設定がfalseの場合と、リトライ数が上限を超えたジョブ情報は dead というキーの sorted set に 失敗時刻が sort 対象の項目として、登録されます。

dead データセットは、オプションの dead_max_jobs, dead_timeout_in_seconds の設定を元に、

失敗したジョブ情報を保持しています。

デフォルトの設定は下記となっています

deed_max_jobs: 1万
dead_timeout_in_seconds: 6ヶ月

となっています。

sidekiq-cron

Sidekiqをcron形式の設定でスケジューリングするgemが sidekiq-cron です。

github.com

設定ファイルの読み込み

YAMLの設定ファイルを読み込む場合は、 config/initializers などに

Sidekiq::Cron::Job.load_from_hash YAML.load_file(#{filepath})

のように指定することで、Redis上に、cronの設定情報が保存されます。

poller

sidekiq-cronでプロセスを持つことはなく、sidekiqのlauncher を上書きすることで実現しています。

# Sidekiq::Lancher

# remember old run
alias_method :old_run, :run

# execute normal run of launcher and run cron poller
def run
  old_run
  cron_poller.start
end

sidekiqの manager, poller とは別に、 cron_poller の処理を start しているのが解ります。

cron_poller の 処理は、上記でRedisに取り込んだ cron の設定を定期的に見に行って、ジョブキューへ追加する処理をやっています。

TCPのウィンドウ制御とコネクション識別

ウィンドウ制御

TCPは送信と応答確認によってデータ送信の信頼性を高めているが、パケット毎にそれをやっていると効率が悪い。

この問題を解決するために、ウィンドウ制御という概念を取り入れている。

具体的には、応答確認をする単位を1パケットではないく、ある一定サイズ(ウィンドウサイズ)ごとに行うようにすることで、通信を効率化している。

サイズの目安

  • 1パケット 1.5KB (イーサネットの最大パケットサイズ)

  • ウィンドウサイズの最大 64KB このサイズをスケールさせる規約もあり、近代のOSは対応しているらしい(気が向いたら調べる)

ASCII.jp:帯域を効率的に利用するTCPの仕組みとは? (1/2)|TCP/IPまるわかり

コネクション識別

TCPヘッダには接続IDのフィールドがないので、どうやっているのか気になったので調べてみた。

2つのendpointのIP:portで識別している。

The TCP/IP Guide - TCP Ports, Connections and Connection Identification

initialPropsとstate - React.js

Props in getInitialState Is an Anti-Pattern | React

getInitialState内でpropsを用いてstateを生成するやり方は、どのデータが本筋のデータかがわかりづらくなってしまう状況になってしまうことがあるので、注意して使ったほうがいい。

コメントフォームを例に考えてみる。

var CommentBox = React.createClass({
  getInitialState: function(){
    return {
      comment: this.props.comment
    }
  },
  handleChange: function(e) {
    this.setState({comment: e.target.value})
  },
  render: function() {
    return <div className='comment-box'>
      <textarea 
        onChange={this.handleChange}
        defaultValue={this.state.comment} />
    </div>
  }
})

上記のようなコードの場合、props.commentとstate.commentがあり、コードの複雑性が増してくると、どちらがどのケースで本筋のデータかがわかりづらくなってしまう可能性がある。

なので、下記のようにpropsは明示的に初期化用のデータということを表す名前にしたほうが良い。

var CommentBox = React.createClass({
  getInitialState: function(){
    return {
      comment: this.props.initialComment
    }
  },
  handleChange: function(e) {
    this.setState({comment: e.target.value})
  },
  render: function() {
    return <div className='comment-box'>
      <textarea 
        onChange={this.handleChange}
        defaultValue={this.state.comment} />
      <button onClick={this.handleUpdateClick}
    </div>
  }
})