Professional Rails on ECS (rails developer meetup 2017)

このエントリはRails developer meetup 2017で発表した内容をブログとして書き出したものです。 サンプルのスニペットが多いので資料の代わりにエントリとして公開します。 スライド用のmarkdownを元に起こしたものなので、少し読み辛いかもしれませんがご容赦ください。

ECSとは

  • Dockerコンテナを稼動するためのクラスタを管理してくれるサービス
  • 使えるリソースを計測し、自動でコンテナの配置先をコントロールしてくれる
  • kubernetesではない。最近、kubernetesが覇権取った感があって割と辛い
  • 今はEC2が割とバックエンドに透けて見えるのだが、Fargateに超期待
  • ECS or EKS :tired_face:

RailsアプリのDockerize

オススメの構成

  • 実際にデプロイするimageは一つにする
    • 例えばstagingやproduction等のデプロイ環境の違いはイメージでは意識しない
  • 手元で開発する様のDockerfileは分ける

FROM ruby:2.4.2

ENV DOCKER 1

# install os package
RUN <package install>

# install yarn package
WORKDIR /yarn
COPY package.json yarn.lock /yarn/
RUN yarn install --prod --pure-lockfile && yarn cache clean

# install gems

WORKDIR /app
COPY Gemfile Gemfile.lock /app/
RUN bundle install -j3 --retry 6 --without test development --no-cache --deployment

# Rails app directory
WORKDIR /app
COPY . /app
RUN ln -sf /yarn/node_modules /app/node_modules && \
  mkdir -p vendor/assets tmp/pids tmp/sockets tmp/sessions && \
  cp config/unicorn.rb.production config/unicorn.rb

ENTRYPOINT [ \
  "prehook", "ruby -v", "--", \
  "prehook", "ruby /app/docker/setup.rb", "--" ]

CMD ["bundle", "exec", "unicorn_rails", "-c", "config/unicorn.rb"]

ARG git_sha1 # どのコミットなのか中から分かる様にする

RUN echo "${git_sha1}" > revision.log
ENV GIT_SHA1 ${git_sha1}

docker build


assets:precompile

RailsのDocker化における鬼門の一つ

  • S3 or CDNを事前に整備しておくこと
  • ビルド時に解決するがビルド自体とは独立させる
  • docker buildした後で、docker runで実行する

  • ビルドサーバーのボリュームをマウントし、assets:precompileのキャッシュを永続化する
  • キャッシュファイルが残っていれば、高速にコンパイルが終わる
  • manifestをRAILS_ENV毎にrenameしてS3に保存しておく この時、コミットのSHA1を名前に含めておく。(build時にargで付与したもの)
docker run --rm \
  -e RAILS_ENV=<RAILS_ENV> -e RAILS_GROUPS=assets \
  -v build_dir/tmp:/app/tmp app_image_tag \
  rake \
    assets:precompile \
    assets:sync \
    assets:manifest_upload

prehook

ENTRYPOINTで強制的に実行する処理で環境毎の差異を吸収する

  • ERBで設定ファイル生成
  • 秘匿値の準備
  • assets manifestの準備
    • さっきRAILS_ENV毎に名前付けてuploadしてたのをDLしてくる

秘匿値の扱い

  • 設定ファイル自体を暗号化してイメージに突っ込む
    • 環境変数で直接突っ込むとECSのconsoleに露出する
    • 値の種類が多いと環境変数管理する場所が必要になる
  • コンテナ起動時に起動環境の権限で複合化できると良い
    • prehookで複合化処理を行う

yaml_vault

https://github.com/joker1007/yaml_vault

  • Rails5で入った、encrypted secrets.ymlの拡張版
  • AWS-KMS, GCP-KMSに対応している
  • KMSを利用すると秘匿値にアクセスできる権限をIAMで管理できる
  • クラスタに所属しているノードのIAM Roleで複合化
  • 設定をファイルに一元化しつつ安全に管理できる
  • Railsの場合、secrets.ymlをメモリ上で複合化して起動できる
    • ファイルに展開後の値が残らない

開発環境

docker-composeとディレクトリマウントで工夫する


version: "2"
services:
  datastore:
    image: busybox
    volumes:
      - mysql-data:/var/lib/mysql
      - vendor_bundle:/app/vendor/bundle
      - bundle:/app/.bundle

  app:
    build:
      context: .
      dockerfile: Dockerfile-dev
    environment:
      MYSQL_USERNAME: root
      MYSQL_PASSWORD: password
      MYSQL_HOST: mysql
    depends_on:
      - mysql
    volumes:
      - .:/app
    volumes_from:
      - datastore
    tmpfs:
      /app/tmp/pids


  mysql:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3306:3306'
    volumes_from:
      - datastore

Macの場合

  • ちなみにボリュームマウントが死ぬ程遅いので、何らかの工夫が必要
  • dinghyかdocker-syncで頑張る
    • どっちも辛い
  • Mac捨てるのがオススメ

俺の開発スタイル

  • 開発用Dockerfileでzshや各種コマンドを入れておく
  • docker-compose run --service-ports app zsh
  • シェルスクリプトで自分の.zshrcやpeco等をコピーしdocker exec zsh
  • ファイルの編集だけはホストマシンで行い、後は基本的にコンテナ内で操作する

set -e

container_name=$1

cp_to_container()
{
  if ! docker exec ${container_name} test -e $2; then
    docker cp -L $1 ${container_name}:$2
  fi
}

cp_to_container ~/.zshrc /root/.zshrc
if ! docker exec ${container_name} test -e /usr/bin/peco; then
  docker exec ${container_name} sh -c "curl -L -o /root/peco.tar.gz https://github.com/peco/peco/releases/download/v0.4.5/peco_linux_amd64.tar.gz && tar xf /root/peco.tar.gz -C /root && cp /root/peco_linux_amd64/peco /usr/bin/peco"
fi

docker exec -it ${container_name} sh -c "export TERM=${TERM}; exec zsh"

デプロイの前に

ECSの概念について


TaskDefinition

  • 1つ以上のコンテナ起動定義のセット
    • イメージ、CPUのメモリ使用量、ポート、ボリューム等
    • 物理的に同じノードで動作する
  • docker-composeの設定一式みたいなもの
  • kubernetesでいうPod

Task

  • TaskDefinitionから起動されたコンテナ群
  • 同一のTaskDefinitionから複数起動される

Service

  • Taskをどのクラスタでいくつ起動するかを定義する
  • ECSが自動でその数になるまで、コンテナを立てたり殺したりする
  • コンテナの起動定義はTaskDefinitionを参照する
  • コンテナが起動したノードをALBと自動で紐付ける
  • kubernetesにも似た概念がある

ECSへのデプロイの基本

  1. TaskDefinitionを更新
  2. Serviceを更新
  3. 後はECSに任せる

ecs_deploy

https://github.com/reproio/ecs_deploy

  • capistrano plugin
  • TaskDefinitionとServiceの更新を行う
  • Service更新後デプロイ状況が収束するまで待機する
  • 更新したTaskDefinitionのrevisionを他のタスクで参照できる
  • TaskDefinitionやServiceの定義はRubyのHashで行う
    • Hash化できれば何でも良いので、YAMLでもJSONでも

Why use Capistrano

  • 既存の資産が多数ある
    • slack通知のフックとか
  • デプロイのコマンドが変化しない
  • 設定ファイルの場所や定義も大きく変化しない

個人別ステージング環境

  • アプリサーバーだけで良いなら容易に実現可能
  • RDB等のデータストアを個別に持つなら色々難しい
  • 弊社はアプリサーバーだけ個別にデプロイ可能
  • データストアを弄る場合はフルセットの環境を使い、そこを占有する

インフラの準備

terraform等で以下のものを準備する

  • ALBを一つ用意する
  • 個人別のサブドメインをRoute53に定義
  • ALBのTarget Groupを個人別に定義
  • ALBのホストベースルーティングを定義

その後capistranoにmemberという環境を定義し、各メンバーが自分の名前でtarget_ groupやTaskDefinitionの名前を使ってデプロイ出来る様に諸々を変数化する


terraformの例

module "acm" {
  source = "../acm"
}

resource "aws_alb" "developers" {
  name = "developers"
  internal = false
  subnets = ["your-subnet-id"]
  security_groups = ["your-security-group-id"]
  idle_timeout = 120
}

resource "aws_alb_target_group" "per_developer" {
  count = "${length(var.users)}"

  name = "${element(var.users, count.index)}"
  port = 8080
  protocol = "HTTP"
  vpc_id = "your-vpc-id"
  deregistration_delay = 0

  health_check {
    path                = "/health_check"
    interval            = 45
    matcher             = 200
    unhealthy_threshold = 8
  }
}

resource "aws_alb_listener" "developers" {
  load_balancer_arn = "${aws_alb.developers.arn}"
  port = 443
  protocol = "HTTPS"
  ssl_policy = "ELBSecurityPolicy-2016-08"
  certificate_arn = "your-certificate-arn"

  default_action {
    target_group_arn = "${element(aws_alb_target_group.per_developer.*.arn, 0)}"
    type = "forward"
  }
}

resource "aws_alb_listener_rule" "per_developer" {
  count = "${length(var.users)}"

  listener_arn = "${aws_alb_listener.developers.arn}"
  priority     = "${count.index + 1}"

  action {
    target_group_arn = "${element(aws_alb_target_group.per_developer.*.arn, count.index)}"
    type = "forward"
  }

  condition {
    field  = "host-header"
    values = ["${element(var.users, count.index)}-*.example.com"]
  }
}

resource "aws_route53_record" "per_developer_app" {
  count = "${length(var.users)}"

  zone_id = "${var.zone_id}"
  name = "${element(var.users, count.index)}-app.${var.domain_name}"
  type = "A"
  alias {
    name = "${aws_alb.developers.dns_name}"
    zone_id = "${aws_alb.developers.zone_id}"
    evaluate_target_health = true
  }
}

デプロイ定義の例

set :rails_env, :staging
set :branch, ENV["GIT_SHA1"] || (`git rev-parse HEAD`).strip
set :slackistrano, false

raise "require DEVELOPER env" unless ENV["DEVELOPER"]

target_group_arn = Aws::ElasticLoadBalancingV2::Client.new.describe_target_groups(names: [ENV["DEVELOPER"]]).target_groups[0].target_group_arn

set :ecs_services, [
  {
    cluster: "staging-per-member",
    name: "#{ENV["DEVELOPER"]}-#{fetch(:rails_env)}",
    task_definition_name: "#{ENV["DEVELOPER"]}-staging",
    desired_count: 1,
    deployment_configuration: {maximum_percent: 100, minimum_healthy_percent: 0},
    load_balancers: [
      target_group_arn: target_group_arn,
      container_port: 8080,
      container_name: "nginx",
    ],
  },
]

Autoscale (近い内に不要になる話)

Fargate Tokyoリージョンはよ!

  • 現時点でECS ServiceのスケールとEC2のスケールは独立している
  • Service増やしてもEC2のノードを増やさないとコンテナを立てるところがない
  • 増やすのは簡単だが減らす時の対象をコントロールできない

というわけでデフォルトで良い方法がない。


ecs_deploy/ecs_auto_scaler

https://github.com/reproio/ecs_deploy

  • CloudWatchをポーリングして自分でオートスケールする :cry:
  • Serviceの数を制御し、EC2の数はServiceの数に合わせて自動で収束させる
  • スケールインの際は、コンテナが動作していないノードを検出して落とす
  • コンテナが止まるまではEC2のノードは落とさない

polling_interval: 60

auto_scaling_groups:
  - name: ecs-cluster-nodes
    region: ap-northeast-1
    buffer: 1 # タスク数に対する余剰のインスタンス数

services:
  - name: app-production
    cluster: ecs-cluster
    region: ap-northeast-1
    auto_scaling_group_name: ecs-cluster-nodes
    step: 1
    idle_time: 240
    max_task_count: [10, 25]

    scheduled_min_task_count:
      - {from: "1:45", to: "4:30", count: 8}
    cooldown_time_for_reach_max: 600
    min_task_count: 0
    upscale_triggers:
      - alarm_name: "ECS [app-production] CPUUtilization"
        state: ALARM
    downscale_triggers:
      - alarm_name: "ECS [app-production] CPUUtilization (low)"
        state: OK

ecs_auto_scaler自体もコンテナに

  • ecs_auto_scalerはシンプルなforegroundプロセス
  • 簡単なDockerfileでコンテナ化可能
  • こいつ自身もECSにデプロイする

まあ、Fargateで不要になると思う


コマンド実行とログ収集

ECSにおいて特定のノードにログインするというのは負けである rails runnerやrakeをSSHで実行とかやるべきではない そのサーバーの運用管理をしなければならなくなる


wrapbox

https://github.com/reproio/wrapbox

  • ECS用のコマンドRunner
    • 半端に汎用性を持たせようとしたんでコードが微妙に……
  • TaskDefinitionを生成、登録し、即起動する
  • 終了までステータスをポーリングし待機する
  • タスク起動権限はIAMでクラスタ単位で管理できる
  • 慣れるとSSHとかデプロイが不要でむしろ楽

default:
  region: ap-northeast-1
  container_definition:
    image: "<ecr_url>/app:<%= ENV["GIT_SHA1"]&.chomp || ENV["RAILS_ENV"] %>"
    cpu: 704
    memory: 1408
    working_directory: /app
    entry_point: ["prehook", "ruby /app/docker/setup.rb", "--"]
    environment:
      - {name: "RAILS_ENV", value: "<%= ENV["RAILS_ENV"] %>"}

wrapboxで実行したコマンドログの取得

  • papertrailにログを転送し、別スレッドでポーリングしてコンソールに流すことができる
  • 原理的に他のログ集約サービスでも実現可能だが、現在papertrailしか実装はない

default:
  # 省略
  log_fetcher:
    type: papertrail # Use PAPERTRAIL_API_TOKEN env for Authentication
    group: "<%= ENV["RAILS_ENV"] %>"
    query: wrapbox-default
  log_configuration:
    log_driver: syslog
    options:
      syslog-address: "tcp+tls://<papertrail-entrypoint>"
      tag: wrapbox-default

db:migrate

capistranoのhookを利用しwrapboxで実行する

def execute_with_wrapbox(executions)
  executions.each do |execution|
    runner = Wrapbox::Runner::Ecs.new({
      cluster: execution[:cluster],
      region: execution[:region],
      task_definition: execution[:task_definition]
    })
    parameter = {
      environments: execution[:environment],
      task_role_arn: execution[:task_role_arn],
      timeout: execution[:timeout],
    }.compact
    runner.run_cmd(execution[:command], **parameter)
  end
end

desc "execution command on ECS with wrapbox gem (before deployment)"
task :before_deploy do
  execute_with_wrapbox(Array(fetch(:ecs_executions_before_deploy)))
end

set :ecs_executions_before_deploy, -> do
  # ecs_deployの結果からTaskDefを取得
  rake = fetch(:ecs_registered_tasks)["ap-northeast-1"]["rake"]
  raise "Not registered new task" unless rake

  [
    {
      cluster: "app",
      region: "ap-northeast-1",
      task_definition: {
        task_definition_name: "#{rake.family}:#{rake.revision}",
        main_container_name: "rake"
      },
      command: ["db:ridgepole:apply"],
      timeout: 600,
    }
  ]
end

db:migrate -> ridgepole

  • migrateのupとdownがめんどい
  • 特に開発者用ステージング
  • ridgepoleならデプロイ時に実行するだけで、ほぼ収束する
  • エラーが起きたらslackにログを出して、手動で直す
  • productionはdiffがあればリリースを停止させる
  • diffを見てリリース担当者が手動でDDLを発行する

テストとCI

以下を参照。 https://speakerdeck.com/joker1007/dockershi-dai-falsefen-san-rspechuan-jing-falsezuo-rifang


コンテナを真面目に運用するためには、結構色々考えることがある

何かしら参考になれば幸いです

如何にしてAsakusaから来た面々はISUCON7の予選に敗北したのか

とてもとても悲しいので、とりあえずやったことと言い訳を書いて気を紛らわせることにする。 敗北した身でグダグダ言うのが格好悪いことは百も承知だが、人間には魂の救済が必要であることをご理解いただきたい。

序盤〜方針決定

最初パスワードのコピペミス等でサーバーからガンガンBANされて、そもそもログインできなくなる。これで10分から20分ぐらい無駄にした気がする。 テザリングにIPを切り替えたり、他のノードから入ったりして、何とか公開鍵でログインできる環境を整える。

適当にベンチ流してスコアを取る前に、nginxのログ設定や構成を確認しalpを使って集計できる準備を整えた。デフォルト実装とRuby実装でベンチを流す。その裏で実装を一通り読む。

f:id:joker1007:20171023022330j:plain

ざっくり図を書いて、相談。とにかく/iconsを何とかしないと話が進まないので、静的ファイルとして書き出してCache-Controlだよね、までは即決。

モリスさんの発案により全てのノードにPOSTが来た時点でPUTリクエストを転送し、WebDAVで全てのノードにファイルを書き出して、後はnginxでサーブすればいいという方針で行こうということになる。この決定は後になって思うと危険な判断ではあった。

そこに多少の準備がかかるので、その間にSQLやアプリケーションの実装周りで明らかに非効率な所を潰すことにした。俺はRubyコードの全般を直し、やんちゃさんにDB周りのインデックスやSQLを見てもらった。

中盤

/iconsの静的ファイル化が完了し、nginxにより直接サーブが可能になる。この時点でスコアが3万前後。画像ファイルのキャッシュとかは一切してないので、周りのスコアとかを見ても、まあこんなもんだろうと思う。

ここで一つ目の壁にぶち当たる。nginxにexpiresを追加してCache-Controlが動作する様にしたのだが、スコアが伸びない。この時点でベンチマーカーが一体何であるのかを考えることになる。

ここで我々のチームは、ベンチマーカーがCDNかキャッシュプロキシの様なものからのリクエストである、ということをすぐに想定できなかった。少なくとも自分はブラウザかAPIのクライアントの様なものしかすぐに想定できなかった。

ただ、ネットワークがサチっていることはメトリックから明らかであり、周りのチームとのスコア差を考えると、304かCache-Controlによるキャッシュしか方法が無いだろうというのは分かっていた。

その間に裏でワーカーの数やらキャッシュを調整し、スコアを45000ぐらいまで伸ばす。やったのは大体以下の様な感じ。

  • チャンネルは作ったら変更されないので、起動時に全部Redisのハッシュにストア
  • チャンネルが保持しているメッセージ数もRedisにハッシュでストアしメッセージが入ったらhincrbyする
  • db.prepareを全てmysql2-cs-bindのxqueryに置き換える
  • RedisとMySQLのコネクションをコネクションプール化し起動時に接続済みにしておく
  • nginxとpumaのプロセス数を2にして、スレッド数やwocker_connection数を調整
  • カーネルのsomaxconnが128で少なかったので数を増やす
  • SQLのクエリワークロードに合わせてインデックスを張る (そんなにやることはなかった)
  • N+1をJOINに変えて潰す

ちなみに、pubsubとか使えそうかなとは考えたが、3台でそれやってもなあというのと、短時間で実装するのが無理っぽかったので止めた。

しばらく悩んだ後にモリスさんが気付いてCache-Controlにpublicを突っ込んだらスコアが倍ぐらいになり、どうもCDNっぽいという話が出てきた。

終盤

Cache-Controlがある程度効いているのだが、やっぱりネットワークがサチってスコアが10万辺りで止まる。304を返して何とかする方法があるはずだ、と思ってパケットキャプチャをしてクライアントの動きを見たのだが、どうもIf-Modified-SinceやらIf-None-Matchが見当たらなかった。(本当は少数ログに残ってた可能性はあるが気付かなかった)

で、どうやって304で返すかで色々と迷走することになる。この時にベンチマークの仕様について確信が持てなかったため、肝心なことに気付けなかった。EtagやLast-Modifiedが入ってて、何故ネットワークが先にサチるのかが分からなかった。

その間も裏でアプリケーションのキャッシュ可能な場所を探したり、nginxのopen_file_cacheやらTCP周りのカーネルチューニングをやって11万ぐらいは安定して出る様になっていた。

しかし、最終的に最後まで304でレスポンスを返すことができず、アプリケーションやDBにかかっている負荷は5割に満たないまま時間切れとなった。

アプリケーションサーバー側に来ていたトラフィックは遅くても100msec、POSTでも200msecぐらいで、半数以上は数十msecで返せていたのでウェブアプリケーションとしては充分な水準だったとは思う。

最終的な敗因

終わった後の情報からの推測によると、一番致命的だったのは静的なファイルを返す様にした際に各ノードに初期画像を撒いたのだが、その時にファイルの更新時間を揃えるのを忘れたという点だと思われる。

ブラウザだったら、Cache-Controlで一旦ローカルにキャッシュされてしまえば、If-Modified-Sinceが多少異なっていても、そもそもサーバーにアクセスしないので、何も問題は無い。

しかし、後から推測するにベンチマーカーはCDNからのリクエストを想定しているものだと思われる。なのでレスポンスヘッダの状態をちゃんとチェックしてキャッシュの削除をしていたんじゃないだろうか。

もし、ファイルの更新時間が異なる同一ファイルが各ノードにあった場合、あるノードから戻ってきたLast-Modifiedが自分がリクエストしているIf-Modified-Sinceより前になる場合、というのが発生する。ここの値が一致しないとnginxはデフォルトでは普通にコンテンツを返すはず。

ここで、一つ気付いていながら確信が持てず試さなかったのが、nginxのif_modified_since beforeである。もしサーバーが持っているファイルの更新時間より未来のIf-Modified-Sinceが送られてきても、nginxは304を返すことができる設定がある。恐らくうちの場合はこれでほとんどのコンテンツに対して304を返すことができた可能性がある。

一方でEtagもレスポンスヘッダに含まれており、ファイルの更新時刻が異なる場合それも異なってしまうので、最終的にはnginxの実装に依る様な気もする。どっちにしても駄目だったかもしれないが、そこまでの知識はない。

何にせよ、上記の問題によって、初期画像に対するリクエストのほとんどが304を返すことができなかった。

そのためネットワークがサチって負荷が上がりきらずスコアが伸びなかった。3台分の帯域をフルに使ってWeb,DB共にリソースの半分以上の余力を残して11万点だったので、304がまともに返せていれば、20万後半から30万ぐらいは行けたんじゃないかとは思うのだが、完全に別の世界線の話になってしまったので、我々には本当のところは分からない……。

ちなみに、WebDAVでファイルを複製して各ノードに配置したことが危険だったのは、ファイルを書き込んだタイミングが1秒以上ズレると上記と同じ問題が発生するところだった。

そもそもファイルの更新時間という点に気付いていなかったので、ここも致命的だったのかどうかは定かではない。ローカルネットワークは帯域もレイテンシも全然余裕であり、書き込みが終わるまでに全体で0.1秒以下ぐらいしかかかってないので、よっぽど運が悪くない限りはそうそうズレることは無いと思っていたのだが、ぶっちゃけ各ノードの時計が秒単位で一致してたかというと、はなはだ怪しい。恐らくこれも影響があったのではないだろうか。

言い訳

実際、適当にいくつかの画像にアクセスした限りでは複数のノードでもEtagもLast-Modifiedも一致していたのも気付けなかった点の一つではある。

しかし、今回私にとってとてもハードルが高かったのは恐らくCDNというものを想定したベンチマーカーだったこと、そしてその挙動特性を知る経験が無かったことだ。

一応、数年Webに関する仕事をしてきて、今CTOなんぞをやっている訳だが、CDNが必要になったことなど今迄一度も無かった。ずっとB2Bで仕事をしていたのでPOSTの数が多いとかやたら複雑だとか一回のGETでめちゃめちゃ複雑な計算結果を返さなきゃいけないことは一杯あるのだが、GETはそんなに世界のあちこちから来ないし、1分に数件とか処理できれば余裕な仕事がほとんどだった。

というわけで、CDN等は遥か遠い世界の話だった。一応メルカリの事件とかは知ってて、ああやべえなCache-Control周り、とかは思ってはいたのだが、所詮対岸の火事であるという認識で、必要になった時に調べられる様に覚えておこう、ぐらいのものだった。

この辺りのハイトラフィックWebに対する地力の無さが敗因だったと言える。

一方で、運が悪かったというか問題の特性によって敗北した点もあると思っている。いくらいきなり無茶なパフォーマンス改善が降ってきたとして、そのサービスがCDNを使っているかどうかと、どういうCDNを使っているかの情報すら無いままサーバーサイドの改善にかかることはまずあり得ないだろうと思う。(まあ、この業界は想像の斜め下とか余裕で越えてくるので分からんけど)

またCDNの導入がされててS3やGCSに類するオブジェクトストレージが使えないというのも、イマイチ考えにくい。今回、ローカルのファイルシステムから画像をサーブした結果ファイルの更新時刻の罠にハマってしまったが、仕事で関わったサービスで画像をサーブするのにローカルのファイルシステムを使ったことは多分一度もなかったと思う。CDNを使うぐらいまで成長したサービスが、そういう環境が無いってのはそんなに多くはないだろう。(オンプレで開発してきて場当たり的な改善とCDNでギリギリ何とかしながら成長してしまったサービスとかならあり得ないとも言えない気はする)

というわけで、今回の問題は本当に世界レベルからのアクセスがあるB2CのハイトラフィックWebと向き合ってきた人間がやはり有利だったと思う。まあ、それがWebサービスの現実であり地力の違いである、と言われればグウの音も出ないのだが……。あー辛い……。

今回の我々のチームは全員がB2B業であり、ミドルウェアやら集計SQLやら分散処理システムやらワークフローを見るのがメインみたいな人間ばかりで、これは完全に最近のWebの現実というものに立ち向かえていなかったと言える。CDN使っててS3の無い世界なんて知らねえよww

まあ、私の様な人間はクラウドのぬるま湯に漬かって生きているのだなあ、ということを実感した1日ではあった。

今回得た教訓は、CDNの仕様は厳密に確認しなければならないということと、ローカルファイルシステムからコンテンツをサーブする時はファイルの更新時間を揃えなければいけない、ということだ。まあ後者の知見はよっぽどでないと使う機会は無さそうだが。(未来のISUCONとかぐらいっぽい)

いやー、S3って本当便利ですよね。

データ分析基盤構築入門の献本をいただいた

先日、著者のデータ分析基盤構築入門の著者の一人である@yoshi_kenさんより、献本をいただきました。

http://gihyo.jp/book/2017/978-4-7741-9218-5

f:id:joker1007:20171006045932j:plain

この本は、Fluentdと、ElasticSearch, Kibanaを中心にしたデータの可視化と分析を行える基盤を実際に構築する手順やその意義等が詳細に書かれた本です。

少し遅くなってしまったのですが、やっと一通り目を通すことができました。

序盤はデータ分析を行うことの意義や、分析を行うためのデータ設計、ETLやデータフローの構築が何故必要なのか丁寧に解説されています。最近私も似た様な分野で仕事をしているので、その通りだなあと頷くことが多い内容でした。

中盤はfluentdの解説、後半はElasticSearchとKibanaについての解説が中心です。かなりconfigサンプルが多いので始めてこういうミドルウェアを触る人にとっては助けになる内容だと思います。

fluentdはv0.12系で書かれていて、現在でも安定板はまだ0.12系の扱いですが、ちょうどv0.14系への移り変わり時期と被ってしまっているのが、残念な所です。軽くフォローはされていますが、基本的な内容はv0.12系をベースに書かれています。私も技術書を書いたことがあるので想像できますが、こういうバージョン移行期の執筆は書く側としてももどかしい思いをする所だと思います。しかし、ほとんどの内容はそのまま活用できますし、fluentdのアーキテクチャや構成パターン、運用TIPS等、幅広く網羅されていて、とても良い内容でした。

そして、最後にAppendixとしてfluentdのプラグインまとめとembulk, digdagについての解説があるんですが、Appendixが異様に分厚くて、これは普通にパート1つ分ぐらいある様なボリュームでしたw 多分、日本語の書籍でfluentd, embulk, digdagについてまとめて解説してる本は貴重だと思います。

embulkの解説の中で、私が作ったembulk-filter-ruby_procを活用している例が紹介されていて、なるほど、ちゃんと誰かの役に立ってるんだと嬉しくなりました。ご紹介いただきありがとうございます!

プラグインまとめでは、私がメンテしてたり作ったりしているプラグインもいくつか紹介されています。この本を読んだ人にとって使えそうなものがあったら幸いですね。

私の仕事でも、fluentdやembulkはかなり活用しており、この本のfluentdの解説はとても手厚いので同僚にオススメしたい感じでした。

今回は、献本していただきありがとうございました!

(そういえば、技術書の献本をいただいたのは初めてかも)

RubyKaigi 2017の記録

先日、広島で開催されたRubyKaigi 2017に行ってきました。

相変わらず各トークのテクニカル度合いが異常にハイレベルで刺激的なカンファレンスでした。

最近、静的解析やRubyと型の関係がホットトピックであることもあり、ripperやparser gem等のRubyパーサやRubyVM::Iseq周りを触っているスピーカーが多く、コアに突っ込んでいく人のアクティビティにはとても刺激を受けました。 個人的にはIseqのバイナリ表現とソースコードの変換を活用してマクロの様なことを実現していたCompiling Rubyという発表が印象的でした。 後、毎年ラストのキーノートはやってることが凄過ぎて、圧倒されるばかりですね。

私自身は、メインのスピーカーとして登壇することはできませんでしたが、LTスピーカーとして話をする機会がもらえたので、そこで登壇してきました。

f:id:joker1007:20170927024108j:plain From RubyKaigi 2017 | Flickr

壇上から写真が撮れるのは嬉しい。 f:id:joker1007:20170919174020j:plain

内容は、Refinementsの貴重なユースケースと、Refinementsにbinding_ninjaという拙作のgemを組み合わせた黒魔術の例を示す、という話でした。

binding_ninjaは、C Extensionとして実装することで、メソッドの呼び出し元のコンテキストのbindingを暗黙的に引数に組み込んで渡す、というgemです。 何のことやら良く分からんと思うので、リンク先のREADMEを読むとどういうものか伝わるかと思います。 binding_of_callerという有名なgemがあるのですが、それの機能限定、高速化版みたいなものです。

後から聞いた感じだと、喋りの勢いと技術的なマニアックさが良い按配だったらしく、割と好評だったみたいです。いやー、ウケて良かったー……。

また、2日目の夜は多くのDrink upが開催されてましたが、私は永和システムマネジメント様がスポンサードしていたDrink upでスタッフとして日本酒の選定をさせてもらいました。

日本酒を大量に準備してのDrink upは二度目で、前回もとても好評だったことや、永和というネーミングバリューもあり、一般参加者の募集は即日完売だったらしく、いやースタッフで良かったーと思いましたね。

RubyKaigi 2日目で「Food, Wine and Machine Learning: Teaching a Bot to Taste」というタイトルで登壇していたMai Nguyenさんが参加してくれて、ワインを愛する彼女に日本酒を振る舞うことができたことを嬉しく思っています。

Drink upも好評に終わって最高の夜でした。

f:id:joker1007:20170919185609j:plain f:id:joker1007:20170919185615j:plain

今回、お店側のスタッフの人がかなりサポートしてくれて、同じグループの日本酒バルのお店からメンバーを派遣してくれた様で、お酒を振る舞う上でめちゃくちゃ助かりました。ちゃんと味を見た上で分かり易くグルーピングして並べてくれたし、好みの味に対するお酒のリコメンドもしっかり応えてくれました。

名刺を貰ったので、もしまた広島に行く機会があれば、是非足を運びたいお店ですね。

f:id:joker1007:20170927180620j:plain

その後、Ruby Karaokeまで参加し、大体最後まで居たんですが、ここでめちゃくちゃ消耗して3日目のセッションの半分以上を聞きそびれるという失態を犯すことに……。 まあその後、海外から来てRuby Karaokeに参加してくれた人に軽くお礼の言葉を貰ったんで、まあいいかーという気になりました。救われた感じw

結局、3日目の夜も大体朝方までデカ外人(@dekagaijin)パークで飲んでいて、ヘロヘロのまま宿に戻ったら、チェックアウトが10時だったので9時ぐらいに起きる羽目になり、ロクに広島観光をする余力もなく、お好み焼きだけ食べて東京に戻る、という感じでした。 本当は、もうちょっと宮島ぐらいまで行ったりしたかったんですが、家まで帰り着いた時の自分のコンディションを考えると、帰って正解だったな、と思う。

来年は仙台ということで、東京からだと広島より行き易いし、ジョジョの聖地である杜王町についにRubyKaigiが!って感じなので、何とかCFPに応募できるぐらいは頑張りたいと思います。

穴子飯美味かった。 f:id:joker1007:20170918213147j:plain

穴子刺とハモとメゴチ。ハモは西日本に限る。 f:id:joker1007:20170918202018j:plain

辛いのはそんなに得意ではないが山椒は好きなので、広島の汁なし担々麺は良かった。 f:id:joker1007:20170918120329j:plain

広告を非表示にする

私的リモートワークの良い点、悪い点

Twitterでちらっと見かけたので、自分も良い点と悪い点をまとめてみようと思う。

良い点

  • 電車に乗らなくて良い
  • ミーティング開始の5分前まで寝てられる
  • 人の話し声がしない
  • 話しかけられずに済む
  • 疲れたらいつでもベッドにダイブできる
  • ちゃんとアウトプットしてれば、仕事している様に見せなくてもいい
  • 寝間着で仕事ができる
  • ハイスペックなPCが使える
  • WiFiが不安定みたいなトラブルが少ない
  • 良い椅子が使える
  • ヘッドホン無しで音楽が聞ける
  • いきなり歌い出しても、誰も変な目で見ないし、迷惑がかからない

基本的に引き篭りなので、自宅の環境が快適になる様に腐心した結果、大抵のオフィスより自宅の方が執務環境として優秀であり、自宅より仕事に関する物の水準が高い会社というのをほぼ見たことがない。

そして、日常生活において、歌うというのが精神の健康上とても大事なので、音楽を聞きながら突然歌い出したりしたいのだが、出社してそれをやると頭が致命的な人に思われてしまうし人に迷惑がかかるので、これが最も重要と言えるかもしれない。 カーレンジャーのOPとか聞いてたら「レッツゴー!」とか言いたいし、キングゲイナーのOP聞きながら「キング、キング、キングゲイナー」って歌わずに済ます方が難しい。(そんな曲を仕事中に聞くな、という話であるが……) 実際、すげー辛いテストコードの修正作業とか無意識で歌えるテンション高めの曲とか歌いながら無理やりドライブすると、結構進捗するので勢いってのは大事だと思う。

悪い点

  • チームメンバーとのコミュニケーション量は減る
  • たまに飲みにいく話になった時に、場所が渋谷とか新宿だと行くのが面倒くさい
  • ギリギリまで寝てようと調整し過ぎて寝坊する、疲れたので仮眠を取ろうとして寝過ぎる
  • 生活リズムが無限にズレていく
  • 仕事を止めるタイミングが無いので無限に仕事をしてしまう
  • 飯を食わなくなり、アルフォートで生活する様になる
  • 運動不足が加速する
  • 職場までの距離に対する我慢が足りなくなってくる
  • 歌いながら仕事をすることに慣れてしまい、たまに外に出た時に突然歌い出す危険が増す

一番問題なのは仕事し過ぎではないかと思う。中断するタイミングも特に無いので飯を食うのが面倒臭くなり、冷蔵庫にストックしてあるアルフォートに頼ることになる。幸いにして家の近くにタリーズがあるので、考えごとやら軽く本読む時にタリーズまで行ってついでにサンドイッチ食ったりする様にしている。

生活リズムのズレを是正する仕組みも無いので、自分の睡眠傾向だと生活リズムがえらいことになる。最近は朝の7時から8時に寝て12時から14時に起きることが多い。半年前ぐらいまでは5時には寝れてたんだが……。そして、ミーティングに合わせて無理やり起きたりするので、かなり辛いことになる。

総じて肉体にじわじわダメージが蓄積されていく。自分がどうしても不健康な生活に向かっていくからなのだが、意思の力とかではどうしようもない感じ。

しかし、基本的に肉体にダメージが溜まるのは根本的に自分の体質やら生活スタイルの問題であり、大なり小なり出社してても同じことなので、仕事をするという点においては、やはり自宅が快適なのであまり自宅から離れたくはない。

Ryzenで久々にPCを1台組んだ

Thinkpadが唐突にぶっ壊れたため、自宅のPC環境を早急に何とかしないと色々不便でしかたなくなってしまった。 最近、Ryzenが盛り上がってるし、ここ5年ぐらいデスクトップPCを新調していなかったので、もうこのタイミングで1台作ってしまうことにした。

ちなみに、予算に限界があったため、タイミング的にThreadripperで組むことも可能だったがそれは諦めた。

構成

パーツ 名称
CPU Ryzen 1800X
マザー MSI X370 GAMING PRO CARBON
メモリ Crucial Ballistix 16GB x2
NVMe CFD CSSD-M2O512PG1VN 512GB
VGA GIGABYTE GeForce® GTX 1070 G1 Gaming 8G
電源 Seasonic X Series 860W SS-860XP2S
クーラー Corsair H110i (簡易水冷)
ケース Thermaltake Core v4l

f:id:joker1007:20170822041403j:plain

所感

ツクモで安売りしてた電源とマザーとRyzenのセットを買って、後は適当に集めた。 大体、これで25万ぐらいか。 再利用できるものが何も無かったんで、全部買い集めたら結構かかってしまった。 割とかかってしまったので、GPUは日和ってGTX 1070にした。まあどうせそんなに激しく使わないだろうし。 ケースを持ち帰る時に、あんまりでか過ぎると重くて辛いので、程々のサイズのミドルタワーにしたら(それでも持ち帰るのは辛かった)、ビデオカードが3.5インチベイに干渉する問題があった。幸い取り外しが効くもので、3.5インチベイにそんなHDDを突っ込む予定も無かったので、ベイを取り外して事なきを得たが、ちゃんと長さを測っとくべきだった。最近のビデオカードがあんなでかいとは……。

OSはもちろんGentooを入れた。

ちなみに、Gentooなので日々バリバリコンパイルしているが、噂のSEGVは起きてない。やっぱあれ初期ロットの問題かBIOSがこなれてなかっただけなんじゃないだろうか。 AGESA 1.0.0.6 ファームウェアならメモリコントローラーがかなり安定する、という話があったのでちゃんとBIOSを上げておけば大丈夫な気がする。

ただ、問題が無かった訳ではなくて、唐突にPCがフリーズする問題に数日悩まされた。 ログにも何も出てなかったのだが、ある日syslogにrcu_schedでCPUがstallしているというログが出てたことと、フリーズする状況が長時間のアイドル後に操作しようとした時に頻発してたことから、色々ググってみるとC6-Stateを切ると安定したという報告が見つかった。 いわゆる、CPUのアイドル時の省電力機能だ。 状況やエラーログから、これはC6-Stateからの復帰に失敗している可能性がかなり高いとみて、BIOSで無効にしてみたところ、めちゃめちゃ安定した。 その後は一度も固まってないし落ちてない。超快適。 ハードウェアの初期不良とかでなくて良かった。CPUが悪い可能性はあるが、別にC6-State切った所で困ることはないし。

久しぶりにデスクトップPCを新しくしたら、今までのノートPC生活から劇的に性能が向上して超快適である。 やはりデスクトップPCぐらいの処理能力は欲しい。 特にカーネルやブラウザのコンパイルがこんなに早く終わるとは、Gentoo使いにとってはとても嬉しい。SEGVさえ起きなければ。(自分の環境では起きてないから今のところ問題ない)

というわけで、快適なPCが手に入ったが、今までずっとノートPCだったのでWebcamとかスピーカーも無い(イヤホンはある)ことに気付いて、色々と買い足している。 思ってたよりかなりお金がかかってしまったが、動作感やスペック的にはかなり満足のいく感じになったので、とりあえず良かった。

パーフェクトRuby第二版 こぼれ話

書籍紹介は既にいくつか書かれているんで、私は自分の担当した箇所の話を書こうかと思います。

本日(5/17)改訂2版 パーフェクトRubyが発売されます - すがブロ
改訂2版 パーフェクトRubyが出版されました - esm アジャイル事業部 開発者ブログ
改訂2版 パーフェクトRuby:書籍案内|技術評論社


私は、どっちかというと大きく書き直す所をメインで担当していました。主にテストコードの章です。後Refinementsについても少し書いてます。
テストコードの章では、書籍紹介にある様にtest-unitを採用しています。
fluentdプラグイン関連のテストコードはtest-unitで書かれていることが多いのですが、最近その辺りを結構触っているので、私が書きますよと手を挙げさせていただきました。
Refinementsについては、恐らく数少ないproduction環境でもRefinementsを活用しているものとして、貴重なユースケースを提供しておこうという意図があったので書かせてもらいましたw
私はRefinementsとても好きなんですが、実装側の苦労とか機能の限定が厳しいので、あんまり良い評判を聞かないですね……。
もちろん、そんな上手いこと使えるケースはそう多くないので、無理して使うのは止めようね、とは書いてありますw

test-unitを採用した理由ですが、この本はRubyという言語自体をテーマにした本なので、バンドルされているテスティングフレームワークを採用して書くのが入門者にとっても分かり易いだろうと思ったからです。
また、通常のRubyコードにかなり近く、特化したDSLについての事前知識を説明しなくてよかったので、内容をコンパクトに収めることができました。
ちなみに、私個人としてはRSpecは結構好きなのですが、RSpec3系になって嫌う人の気持ちも分かりますw
Power Assertも好き嫌いあると思いますが、これも言語自体にバンドルされていて、自分も関連gemを作ったりpull reqを出したりしたこともあって、ちゃんと触れています。


実際、一番しんどかったのはテストコードを書くためのサンプル実装を用意することと、モックを使ったテストを行う理屈をサンプル実装の中に用意することでした。
シンプルで読むのに苦労は無い、がテストを書くことが役に立ちそうで、リファクタリングの余地がありそうな程度に冗長なコードで、モックやスタブを紹介するためにそういうテストを書く理由が存在するコードを用意しなければならないわけです。
色々頭を捻った結果がこの本に書かれているわけですが、上手くいってるかどうかは読んでくれた人がどう受け取るか次第って所でしょうか……。
突っ込みを入れたい人は、是非、書店やgihyoさんの電子書籍サイト等でお求めください!