nyamadoriの日記

コードだけは綺麗に書きたい。

よくコケる不安定な feature spec への対処療法 rspec-retry gem

背景

業務で開発している Web サービスの feature spec が非常に不安定で、feature spec が CI 上でコケるたびに、ビルドジョブをよく手動で再実行していました。1 回の再実行でパスすることもあれば、まれに 10 回以上の再実行が必要なこともありました。

その Web サービスでは、CI のジョブ終了までに約 8 分(中央値)かかります。つまり、リトライの回数が増えるたび、大体 8 の倍数分リリースに要する時間が増加してしまいます。

このような状態だと、価値をユーザに早く届けられなくなってしまうし、個人的にもフラストレーションがたまっていたので、 2 ヶ月前に rspec-retry gem を導入しました。

rspec-retry gem

rspec-retry gem は、RSpec の example に :retry オプションを追加し、spec が成功するまでオプションで指定した回数だけリトライしてくれる gem です。

今回は、fail しやすい feature spec 全体に対して、retry オプションを付与するようにしました。

RSpec.configure do |config|
  # run retry only on features
  config.around :each, type: :feature do |ex|
    ex.run_with_retry retry: 3
  end

  config.verbose_retry = true
  config.display_try_failure_messages = true
end

導入した結果

以下の画像は、その Web サービスの CircleCI の Insights から見られるグラフで、縦軸が完了までにかかった時間、横軸がビルドの実行日時です。縦棒は、すべてのジョブではなく、ある時点でサンプルしたジョブで、緑が成功したジョブ、赤が失敗したジョブを表しています。

結果はグラフの通りで、rspec-retry 導入以降はジョブ実行中のリトライが効いたことで、手動でリトライを行うことがほとんどなくなりました。素晴らしい!

f:id:nyamadori:20171119192821p:plain

とはいえ、あくまでこの方法は対処療法で、本来ならリトライが必要にならない spec に修正するのが根本対策です。同じように困っている方がいたらお試しあれ。

リンク

ElastiCache + Redis に出てくる概念と、クラスタモードごとの違い

はじめに

Web サイト表示速度向上の一環として、仕事で、ElastiCache + Redis によるキャッシュ層を導入する。 導入にあたり、ElastiCache + Redis で利用するノードタイプ(インスタンスタイプ)や、制限などの事前調査が必要になった。

ElastiCache + Redis は、ノードタイプやクラスタの種類によって、機能サポートが異なり、混乱するところが多々あったため、AWS のドキュメントを参考に、分かりにくいところをまとめた。

これから導入する段階なので、AWS のドキュメントの内容以上のことは書いていません。 運用についてとか、ハマったところなどの記述はありません。

内容におかしいところがあれば教えてください

概念

実際に使うかどうかに関わらず、シャードやレプリカという用語がバンバンでてくるので、まとめる。

f:id:nyamadori:20170912101609p:plain

参考: http://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/UserGuide/WhatIs.Components.html#WhatIs.Components.Nodes

クラスタ (Cluster)

シャードをまとめる論理グループ。複数のシャードを作ることで、シャーディングができる。データはシャード間で分割される。

クラスタごとにキャッシュエンジン(Redis, Memcached)を設定できる。ElastiCache のクラスタには、クラスタモード無効と、クラスタモード有効の 2 種類ある(厳密にはキャッシュエンジンの種類)。

Redis (クラスタモード有効) は、クラスタ 1 つにシャード 1〜15 個持つことができる。 Redis (クラスタモード無効) は、クラスタ 1 つにシャード 1 個持つことができる。

シャード (Shard)

ノードをまとめるグループ。データはノード間で同期され、2つ以上のノードを用いることで、レプリケーションできる。

1 シャードは、読み書きができるプライマリノード 1 個と、読み込み専用のセカンダリノードリードレプリカ)0 〜 5 個を持つ。

ノード (Node)

ElastiCache の最小単位で、保存領域(RAM)を持つ。 設定したノードタイプによって CPU 性能や保存領域のサイズが異なる。

キャッシュエンジンは、クラスタごとに設定できるため、クラスタ以下のノードすべては、同じキャッシュエンジンで動作する。

レプリケーション

ノード間で同じデータを共有すること。 プライマリノードへの書き込みを、プライマリノードに紐づくすべてのリードレプリカへ非同期的に反映する。

以下のメリットがある。

  • 耐障害性の向上
  • 読み込みの負荷分散

シャーディング

データをシャード間で分割すること。

以下のメリットがある(っぽい)。

  • 読み書きの負荷分散
  • コストパフォーマンス向上
    • 小さめのノードを、複数のシャードに分割することで、データ容量を増やしつつ、クラスタ全体のコストを抑えることができる

自動フェイルオーバ

ノードの障害を検知し、障害が発生したノードを新しいノードに置き換える機能。 (フェイルオーバの一般的な意味は: https://www.idcf.jp/words/failover.html

障害時の挙動

  • 障害元がプライマリノードの場合
    • 自動で選択したリードレプリカをプライマリノードに昇格させ、障害が発生したプライマリノードと置き換える。
    • アプリケーションで利用しているプライマリのエンドポイントは、変更する必要がない
    • 昇格にかかる数分間、プライマリノードへの書き込みの一部が失われる
      • シャーディングしている場合は書き込みの一部、していない場合は全ての書き込み(多分)
  • 障害元がレプリカの場合
    • 障害が発生したノードと新しいノードを置き換える
    • フェイルオーバ中、レプリカが本来担う読み込み処理を、プライマリノードが引き受けるので、プライマリノードの負荷が増える
    • アプリケーションで利用しているリードレプリカエンドポイントを、変更する必要がある

参考: http://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/UserGuide/AutoFailover.html#AutoFailover.Overview

用語の違い

API/CLI 、コンソールの間で、用語が異なる場合があるので注意。 ユーザガイドの中でも、用語が揺れているので把握しておく必要がある。

基本的に

クラスタ (cluster) = レプリケーショングループ (replication group)
シャード (shard)   = ノードグループ (node group)

レプリカノードがない場合は、ノードとクラスタが 1 対 1 で対応するので、

クラスタ = ノード

となる。図で示すと AWS マネジメントコンソールが

f:id:nyamadori:20170912101727p:plain

で、API / CLI での用語が

f:id:nyamadori:20170912101737p:plain

という感じ。

参考: http://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/UserGuide/WhatIs.Terms.html

クラスタモードごとの比較

クラスタモードごとに機能サポートに一長一短があるので、慎重に選ぶ必要がある。

Redis (クラスタモード無効)

  • シャーディング:
    • 未対応
  • ノードタイプ等の変更:
    • クラスタ作成後も、ノードタイプ、エンジンのバージョンを変更できる
    • ただし変更中は、読み書き処理がブロックされ、インスタンスが利用できない(試しに、空の t2.micro ノードを t2.small に変更してみると、約 11 分かかった)
    • レプリカの追加は、クラスタ全体を停止することなくできる(っぽい)
    • キャッシュクラスタがあまり利用されない時間帯に行う必要がある
    • 変更処理は、即時実行または、定期用設定したメンテナンス時間帯行われる
    • ノードタイプの変更は、スケールアップしか対応していない(スケールダウンは、新しくクラスタを作る必要がある)
  • バックアップ・復元:
    • S3 へのバックアップ、S3からの復元に対応
    • cache.t1.micro または cache.t2.* ノードではサポートされない
  • 自動フェイルオーバ付マルチ AZ:
    • t1, t2ノードタイプ は未対応

参考

Redis (クラスタモード有効)

  • シャーディング:
    • 1 〜 15 個のシャードを作成可能
  • ノードタイプ等の変更:
    • クラスタ作成後は、ノードタイプ、シャード数、レプリカ数、エンジンのバージョンを変更できない
    • 変更するには、新たなクラスタを作って、データを移行する必要がある
  • バックアップ・復元:
    • 全ノードタイプで S3 へのバックアップ、S3からの復元に対応
    • ただしクラスタレベルでのバックアップのみ対応
  • 自動フェイルオーバ付マルチ AZ:
    • 利用必須
    • Redis バージョンが 3.2.4 以降なら t2 ノードタイプでも対応。
    • 手動でリードレプリカをプライマリに昇格できない

参考

まとめ

  • 雰囲気、自由に使えそうなクラスタ有効モードだが、意外と制約が多い
    • クラスタ作成後に、ノードタイプ、シャード数、レプリカ数、エンジンのバージョンを変更できない
    • 小さいインスタンスを組み合わせてうまく構築できるのなら、コスパがいい(かも)
    • t2 ノードタイプ等の小さいインスタンスの機能サポートが手厚いのは、クラスタ有効モード
  • クラスタモード無効は、ノードタイプ等を変更できるが、読み書き処理がブロックされる
    • ダウンタイムをなくしたいなら、クラスタを複製して、エンドポイントを切り替える必要がある
      • だったら、クラスタモード有効を使ってもあまり関係ない気がする
      • このあたり、簡単にやれる方法があれば教えて欲しいです
  • クラスタモード無効の t2 ノードタイプは、バックアップ・復元ができないので、本番ではあまり使いたくない気がする

次に調べること

Redis クライアントについて

  • 障害時の挙動
    • 接続をリトライすることで生じる、レスポンス遅延とか
    • レプリカのフェールオーバ時に、アプリケーションで指定しているエンドポイントの切り替え
      • 自動でやってくれる方法はないのか

(メモ)フォーム要素に対してはelement.idよりもelement.getAttribute('id')を使うほうがいい

以下、メモ

  • フォーム要素 (HTMLFormElement) は、自身が持つ <input name="x"> に対して formElement[x] のような添字でアクセスできる。
  • <input name="id"> な要素を持つフォームの場合
    • form["id"] には <input name="id"> な要素が入っている
    • なので、フォーム要素に指定した id 属性は、form.id から取得できない
  • フォーム要素の id 属性を取得するときは、form.getAttribute(“id”) を使うほうが良い

書いてて思ったけど、<input name="getAttribute"> な要素を持つフォームだったら、form.getAttribute すらも上書きされちゃうのかな。そんな名前の input 要素、誰が作るんやという話だけど。

Slack風、Rails によるファストユーザスイッチの実装方法 (Devise 未使用)

この記事は 高知工科大 Advent Calendar 2016 の 14 日目の記事です。

概要

趣味で作っている Rails 製小規模向けグループウェアに、ファストユーザスイッチを実装した話。アカウント登録・認証は Devise を使ってサクッと作ることが多いけど、Devise のカスタマイズで心をすり減らしたくなかったので、Devise なしで一から実装してみた。

今回、Devise を使わないセッション管理のベタ実装には、以下を参考にした。この記事の範囲外の細かい話は、Rails Tutorial を見てほしい。

railstutorial.jp

利用技術

ファストユーザスイッチ?

用語が正しいかどうか分からないけど、Slack や Google のサービスは、アカウント間をログアウト不要で素早く切り替えることができる。

例えば Google なら、ヘッダーのアカウントアイコンを選択して、切り替えるアカウントを選べる。2個以上の Google アカウントを持っている人なら、見たことがあると思う。
Slack なら、チームアイコンをクリックすることで、素早くチーム間を行き来できる。

Google f:id:nyamadori:20161213203224p:plain:w300 / Slack f:id:nyamadori:20161213203736p:plain

要件

グループウェアを作っているので、グループの概念を持った Slack を参考にして要件を立てた。

  • アカウント認証には、サインインするグループID (slug)、グループに登録したアカウントの Email アドレス (email)、パスワード (password) が必要
  • グループごとにアカウントで使用するメールアドレスを変えることができる

以上の要件を反映させたのが、次の ER 図*1

f:id:nyamadori:20161213201403p:plain

実装

実装上、細々とした処理がいくつかあるものの、重要な部分を抜き出すと次のようになる。以降、太字で示したクラスについて説明していく。

  • サインイン時の処理
    1. サインインページで認証に必要な情報 (group slug, email, password) を入力
    2. サインインのリクエストを受け付ける
      [SessionsController]
    3. フォームのバリデーションと、フォームの値を元にアカウントを認証
      [Session モデル]
    4. 認証に成功したら、セッション一覧に新しいセッションを追加し、現在のセッションとする
      [Authentication クラス]
    5. グループページにリダイレクト
  • サインアウト時の処理
    1. サインアウトボタンをクリック
    2. サインインのリクエストを受け付ける
      [SessionsController]
    3. 認証に成功したら、セッション一覧から現在のセッションを削除
      [Authentication クラス]
    4. グループページにリダイレクト
  • サインイン後のセッション維持
    1. Rails アプリケーション内のとあるページに遷移する
    2. リクエストに紐づくセッションを用いてアカウントを認証 [Authentication クラス]
    3. 認証に成功したら、リクエストしたページにそのまま遷移

SessionsController

サインイン・サインアウトのリクエストを受け付けるコントローラ。 具体的な認証処理は Session モデル、Authentication concern にまかせているため、SessionsController は大した処理をやっていないが、 全体の処理の流れが理解しやすいので、以下にソースコードを示す。

class SessionsController < ApplicationController
  def new
    @session = Session.new
  end

  def create
    @session = Session.new(session_params)

    if @session.valid? # `session_params` で渡されたフォームの値のバリデーションと、認証を行う。
      sign_in(@session) # `@session` を元に、新しいセッションを生成
      redirect_to group_path(current_group.slug) # グループページにリダイレクト
    else
      render :new
    end
  end

  def destroy
    sign_out(current_session) # セッションを破棄

    if current_group
      redirect_to group_path(current_group.slug)
    else
      redirect_to new_session_path
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password, :group_slug)
  end
end

Session モデル

Session モデルは、ActiveModel::Model を include したクラスになっていて、フォームのバリデーションと、フォームの値を元にアカウント認証を行うのが責務。 アカウント認証はモデルバリデーションとして扱っており、認証が成功すると、該当の Account と Group のレコードをそれぞれ group, account に格納する。これらのレコードは、セッション管理に利用する。認証が失敗すると #errors にエラーメッセージが格納される。

参考元の Rails Tutorial にはないモデルだけど、このモデルが合ったほうが SessionsController がよりスッキリするので、作ることにした ((コントローラで Session モデルのオブジェクトの変数名に session をつけると、元の session オブジェクトが使えなくなってしまうので、注意が必要。名前大事))。

class Session
  include ActiveModel::Model

  attr_accessor :email, :password, :group_slug, :group, :account

  with_options unless: :authenticated? do |auth|
    # 認証が完了している場合は、以下のバリデーションを行わない
    auth.validates :email, presence: true
    auth.validates :password, presence: true
    auth.validates :group_slug, presence: true
  end

  validate :authenticate, if: -> { errors.size.zero? }

  def authenticate
    @group = Group.find_by(slug: group_slug)

    if @group
      @account = @group.accounts.find_by(email: email)

      unless @account&.authenticate(password)
        errors.add(
          :base, :email_or_password_invalid,
          message: 'Invalid Email or Password'
        )
      end
    else
      errors.add(:base, :no_group, message: "No group for #{group_slug}")
    end
  end

  def authenticated?
    !!(group && account) # group と account の両方が非 nil => 認証が完了
  end
end

Authentication クラス

Authentication クラスの役割は、以下の通り。

  • Session モデルオブジェクトを元に、新たなセッションを作成する (Authentication#sign_in)
  • リクエストに紐づくセッションを読み込み、ユーザを認証し、セッションを維持する (Authentication#authenticate!)
  • Session モデルオブジェクトに対応する、セッションを削除する (Authentication#sign_out)

Authentication#authenticate! は、ApplicationControllerbefore_action コールバックで呼び出され、アプリケーション内のページ遷移ごとに実行される。

ファストユーザスイッチのためのセッション管理

ファストユーザスイッチでは、複数のセッションを同時に管理する必要がある。これは、CookieStore に複数のセッションを格納することで実現できる *2 。セッションには以下のようなデータを格納する。

# 現在ログイン中のセッションの配列
session[:sessions] = [
  # 各ハッシュは、Session モデルオブジェクトを元に作られる
  {
    'account' => 2, # アカウントID
    'group_slug' => 'xxx-group' # グループslug
  },
  {
    'account' => 3,
    'group_slug' => 'yyy-circle'
  }
  # ...
]

# session[:sessions] のインデックス。xxx-group にログインしていることを表す
session[:current_session] = 0

session[:sessions] はログイン中のセッション一覧を保持する。Authentication#authenticate! メソッドは、このようなセッションデータを元に、アクセスしてきたユーザを認証する。セッション一覧にないグループにアクセスしようとした場合や、ログイン中にアカウントがグループのメンバーから外された場合は、ログインページへリダイレクトする処理を行う。

Authentication#sign_inAuthentication#sign_out メソッドは、それぞれログイン・ログアウト時にセッションの追加・削除を行う。

以下が Authentication クラスのソースコード。少しに長くなってしまったが、重要なのは前半の Authentication#authenticate!Authentication#sign_inAuthentication#sign_out の 3 つのメソッド。

concern :Authentication do
  included do
    # メソッドをビューヘルパーとして使えるようにする
    helper_method :sign_in_accounts, :sign_in_groups,
                  :current_group, :current_account,
                  :sign_in_any?

    private

    def authenticate!
      initial_session_data
      return unless group_slug # パスに group_slug が含まれない場合は、認証処理を行わない

      cs = session_for(group_slug) # group_slug から Session モデルオブジェクトを取得する

      if cs&.group.member?(cs.account) # アカウントがグループのメンバーかどうかを判定
        session[:current_session] = session_index_for(group_slug)
      else
        sign_out(cs) # グループのメンバーから外された場合は、残っているグループのセッションを削除
        redirect_to new_session_path
      end
    end

    def initial_session_data
      session[:sessions] ||= []
      session[:current_session] ||= -1
    end

    def sign_in(s)
      return if signed_in?(s)

      ## 新しいセッションを追加
      session[:sessions] << {
        'account' => s.account.id,
        'group_slug' => s.group.slug
      }
      session[:current_session] = session[:sessions].size - 1

      # clear cache: sign_in_* メソッドの呼び出し結果を最新の状態にするため、キャッシュをクリア
      @sign_in_sessions = nil
      @sign_in_accounts = nil
      @sign_in_groups = nil
    end

    def sign_out(s)
      # セッションを破棄
      session[:sessions].delete_if { |g| g['group_slug'] == s.group.slug }

      if session[:current_session].to_i >= session[:sessions].size
        session[:current_session] = session[:sessions].size - 1
      end

      # clear cache
      @sign_in_sessions = nil
      @sign_in_accounts = nil
      @sign_in_groups = nil
    end

    def signed_in?(s)
      sign_in_groups.include?(s.group)
    end

    def sign_in_sessions
      @sign_in_sessions ||= session[:sessions].map do |s| # s のキーは文字列なので、気をつける
        Session.new(
          account: Account.find_by(id: s['account']),
          group: Group.find_by(slug: s['group_slug'])
        )
      end
    end

    def sign_in_accounts
      @sign_in_accounts ||= sign_in_sessions.map(&:account)
    end

    def current_account
      current_session&.account
    end

    def current_session_index
      session[:current_session].to_i
    end

    def current_session
      sign_in_sessions[current_session_index]
    end

    def session_index_for(group_slug)
      sign_in_sessions.index { |s| s.group.slug == group_slug }.to_i
    end

    def session_for(group_slug)
      sign_in_sessions[session_index_for(group_slug)]
    end
  end
end

まとめ

簡単にだが、Slack風、Rails によるファストユーザスイッチの実装方法を示した。Devise を使わないセッション管理のベタ実装には、Rails Tutorial を参考にした。

railstutorial.jp

実際にファストユーザスイッチを実装したグループウェア (作りかけ) は、以下のリポジトリにあるので、参考までにどうぞ。

github.com

実装ポイントは、

  • 複数のセッションを配列として格納すること
  • DB には格納しないけど、ActiveRecord と同じインタフェースでバリデーションしたいときは、ActiveModel::Model を include すると便利

ぐだぐだと書いたけど、Rails Tutorial との差分はそれ以外にないという。

雑感

  • セッション管理のベタ実装は、初動の開発が遅れるが、一通り作りきってしまえば意外と簡単
    • ただ、作りきった後に「あー、簡単だったな」となるかどうかは、うまく設計できるかどうかにかかってそう

*1:https://github.com/voormedia/rails-erd で作った

*2:CookieStore 以外を使う方法もあるが、気軽で要件的にも必要十分だったので CookieStore にした

ただの水道水と、沸騰させることでカルキを飛ばした水道水、墨色の違いを見る

概要

淡墨作品を書くときは、水道水を沸かした水で墨を下ろした方が良いという話を、聞いたことがある。 でも、溶媒によって墨色にどういう影響をあたえるのかが分からなかったし、そのあたりをちゃんと説明しているページが見当たらなかった。

そこで、ただの水道水と、沸騰させることでカルキを飛ばした水道水 (以下、脱カルキ水) とで、墨色にどういう違いが生まれるのかを、実際に試してみた。

使ったもの

材料

以下の組み合わせは、手持ちの紙サンプルの中で最も墨色が良かったもの。紙サンプルは書遊で買い物をしたときに一緒についてきた。

道具

墨の溶媒

  • 水道水
  • 鍋で約10分間沸かし、カルキを飛ばした水道水 (以下、脱カルキ水)*1

結果

溶媒: 水道水

文字が頭でっかちで残念だが、墨色だけ見てほしい。意図して残念な字にしたわけじゃない。

  • にじみの部分が平板的。なんかベタッとしている (うまく言い表せられない)
  • 濁ったにじみ

f:id:nyamadori:20161120174901j:plain:w300

溶媒: 脱カルキ水

  • にじみの部分が立体的。基線 (筆が通った線) からにじみの縁に向かって、墨が放射状に広がっている様子が見られる
  • 澄んだにじみ

f:id:nyamadori:20161120174858j:plain:w300

まとめ

  • 立体的で澄んだにじみを出すのには、脱カルキ水を使うと良いと思う
  • ただし、にじみ方は紙と墨の相性によって*2大きく変わるため、まずは、手持ちの墨に対してマッチする紙を見つけるべし
  • なぜ溶媒によって異なる墨色を示すかは、墨に対する知識をあまり持っていないので説明できない。他の文献を当たってほしい*3

おまけ

溶媒: 脱カルキ水 + 4日目の宿墨

下ろした墨を数日寝かせたものを宿墨という。宿墨を使うと、基線がよりくっきりと表れ、にじみが少なくなる。

墨溜まりができると左のように、サラッと書くと右のように線が表れる。

f:id:nyamadori:20161120175956j:plain

溶媒: 脱カルキ水 + 紙:「泰山」

墨と紙の相性によっては、綺麗なにじみが出てくれない。

f:id:nyamadori:20161120190026j:plain:w300

宿墨を使うと、基線がはっきりし立体感が出て、さっきよりはマシになった。

f:id:nyamadori:20161120191010j:plain:w300


追記

水道水、脱カルキ水のどちらが良いor悪いということを言いたいのではありません。「良い」「悪い」があるとすれば、それは作者が求める表現かどうか、ということだけです。どちらの溶媒も使い方次第です。

*1:飲んでみると実にまろやかな口当たりだった

*2:気温や湿度も墨色に影響するらしい

*3:良い文献があればぜひ教えてください!!

「エンジニアなのに書道が趣味って変わっている」とか時々言われるので、きっかけとか書道歴を晒す

f:id:nyamadori:20161029053051j:plain

概要

「エンジニアなのに書道が趣味って変わっている」とか「きっかけは何だったの?」とか、自己紹介すると時々聞かれるのでここに残す次第。

内容は、書道を始めるきっかけから今に至るまで。書道歴とか言いながら段位も賞も何もないけど*1、小さいながらも個展は開いた (後述)

段位とか賞だけが書道じゃないんだ!!!!!!*2

変遷

小学生時代 (高学年ぐらい)

何かの課題でポスターを作ったときのこと。ポスターの文字は、みんな雑にしがちで、絵は良くても文字はヘボいことが多い。それを見て、文字に関しては他の人より絶対うまくやれると思ったのだと思う。だからパソコンのフォントを参考にして誰よりも綺麗に書いた。

結局、文字を綺麗に書いたからといって、課題が評価されることはなかった。だけど、それが「レタリング」だということを、図画工作の先生が教えてくれた。

それ以来、レタリングに興味を持ち、広告や雑誌、街の看板などの書体に目を向けることが多くなった。決して目立たない存在だけど、テキストが持つ空気感や意味合いを変えてしまう書体の面白さに気づいた。

でも、書写 (習字) *3は好きじゃなかった。毛筆で文字を書くなんて今の時代はやらないし、整った字ならパソコンで出力すればいい、と思っていた*4

中学生時代

書き初めの練習中に先生から書道の素質があると褒められる。ホントかよって思ったけど、書道は今も続けられているので、先生の目は間違っていなかったと思う*5。先生の言葉がきっかけで、高校に入ってから書写教室に通い始める。これが書道を始めた直接のきっかけ。

高校生時代

中学校の先生に言われた言葉を胸に、近くの書写教室に通い始める。教室には2年通っていたけど、整った文字を書くことを目的とする書写よりも、芸術表現としての書道がやりたいと思うようになった。

これは蛇足。行っていた書写教室は公文の教室。実は公文は書写もやっている。ビジネス手広い。

公文書写 - お子さまから大人の方まで、くもんの書写は、選べる4教科

公文の回し者じゃないけど、公文書写のお手本は変な癖がなく、原理・原則からちゃんと教えてくれるので良い。先生との相性はあるにせよ、手本に関しては安心できると思う。

大学生時代

芸術表現としての書道がやりたかったので、入学後すぐ書道部に入った。

とはいえ、大学に書道の先生がいないので独学する以外なかった。当時は目標もなくダラダラやっていたので、あんまり上手くなった実感がなかった。ゆるーい部活はそれはそれで楽しかったけど、もっとうまくなりたいという気持ちはいつも持っていた。

道教

学部4年のとき、先輩のつてで先生につくことができた。それから、書の技術が飛躍的に上達したし、以前に増して書道にのめり込んでいった。

週一回の教室では、黙々と臨書*6している。ただひたすら、競書誌の臨書課題を納得行くまで書き込む。「よし、これだ!」と思ったら先生に見せてツッコミを入れてもらう。

小さな個展

そのまま大学院に進み、書道教室に通いながら気楽に書道を楽しんでいたが、修士1年の冬、就活ついでに見に行った独立書展で衝撃を受けた。そこには、今まで見たことのないくらい生き生きとした作品が並んであった。国立新美術館の公募展スペース3階分をすべて使うほどの作品数に圧倒されたし、そのどれもが輝いていた。これほどの規模感、作品レベルは東京以外ではなかなか見られないと思う。

そういう生き生きとした作品群を見て、自分も何か書きたいという気持ちになった。ずっと臨書ばかりして頭が凝り固まっていたけど、自由な創作について考えるきっかけにもなった。独立書展が個展を開くきっかけだった。

大学の図書館で開いた小さな個展だったけど、やってみてよかったと思う。作品の雰囲気がどれも同じにならないように題材を選定して、書きぶりを変えて作品を揃えるのは、とてもクリエイティビティの高い仕事だったし、何より楽しかった。

先生にアドバイスを頂きながらだったで、本当の意味での個展ではなかったかもしれないけど、そうできる経験ではないと思う。知名度も賞も何も持っていないのに多くの方に見てもらえたのは嬉しかった。見に来てくれた方々との出会いは、書道をする上でとても励みになったし、自信を持つことが出来た。

まとめ

  • 小学生時代
    • ポスター作りでみんなが雑にしがちなレタリングに凝る。文字に興味を持った
  • 中学生時代
    • 書道の素質があると先生に褒められる。調子に乗った
  • 高校生時代
    • 書写教室に通い始める。やってみて分かったけど、書写よりも芸術表現としての書道がやりたいことに気づいた
  • 大学生時代
    • 学部4年にして先生に巡り会う。飛躍的に上達した
    • 修士1年の冬、独立書展の作品群に衝撃を受ける。書きたい欲が出てきた
    • 修士2年の夏、小さな個展を開く。見に来てくれた方々との出会いは、書をやる上で励みになったし自信にも繋がった

振り返って

  • 書道の素質があると褒められたのが中学3年、プログラミングを始めたのは中学1年生か2年生ぐらい。中二病って言葉があるぐらい、中学生時代は多感で、その後に大きな影響を与えうる大切な時期だと思った
  • 書写教室で得た知識や技術は、書道をやるようになってからも役に立った。逆も然りであるはず
  • レタリングと書道、プログラミングと書道。デザインと芸術、エンジニアリングと芸術。両方の領域に触れられていることは良いことだと思う。そこで得た感性をどこかに活かせたらと思う。
  • エンジニア×書道の組み合わせは、あまり見かけないのでセルフブランディング的にも書道は続けたい

*1:今のところ、安芸全国書展入選ぐらいしかない

*2:「書道が好き」という話をすると、時々「何段ですか?」とか聞く人いるけど、回答に窮するからやめてほしい。段位制度は会や教室ごとに独自にあるので、所属が違うと比較にならない。だからといって「xx 展入賞」とか言ってもピンとこないし盛り上がらないだろう。やっぱり数にはインパクトがあるんだろうな。「書道八段」とか強そうだし、

*3:書写 (習字)と書道は共通部分はあるけど別物。後述する

*4:今もそう思っている節がある。とはいえ、字がきれいに書けることはまだまだ重宝されるらしい。

*5:上手という意味ではなく、続けられているという意味で

*6:書跡をお手本に練習する方法。文字の形や筆使いを身につける。野球でいう素振りでフォームを身につけるみたいなものだと思っている。野球は全然分からないけど

夏の Speee、内定者アルバイト (インターン) の振り返り

Speee 17卒内定者のやまどりです。夏休みを利用して 1 ヶ月間 Speee でアルバイトしました。入社までの期間を使って実務環境で鍛えさせてくれるとは粋です。

www.speee.jp

やったこと

  • リスティング広告のクローラを作った
    • 自社広告順位や競合広告の分析のために使用される
  • メンテナがいない既存の Java + Akka 製のクローラを Ruby でリプレイス
  • 4 つの出稿媒体に対して設定した数千個のキーワードを一日一回検索にかける
  • 夜 9 時にクロールを開始して朝 6 時までに終えるようにしたい

チーム

  • メインの開発は自分 1 人
  • レビュワーエンジニア 3 人
  • ディレクター 1 人

2 人のエンジニアの方に細かくツッコミ (レビュー) を入れてもらいながら開発していました。主な要件は既存ツールと同じだったため、ディレクターの方と関わることは少なかったものの、仕様や目的について不明点があれば随時聞きに行っていました。

技術選定

紆余曲折あったものの最終的に以下でまとまりました。

振り返り

Good / Keep

  • 既存のクローラでクロールできていなかったところが、正しくクロールできるようになった
  • DB 設計に工数を割くこと
  • Faraday

Problem

  • 思ったより本番環境のメモリ容量が小さく、アルバイト期間終了までに指定のハードウェアでクローラが動かなかった

Try

  • 本番環境の要件を事前に調べる
  • 技術選定時にプロファイラを使う
  • リプレイス案件のときは KPI を設定して、既存のツールからの改善具合を計る

詳しく

[Good / Keep] DB 設計に工数を割くこと

https://gyazo.com/3645df4f7e564a485b2272ab1c4006dd

1 ヶ月間のうち 8 営業日を DB 設計に費やしました。きっちり議論した甲斐があり、その後は設計を修正せずとも開発を進められました。DB 設計がアプリケーションの地盤を固めてくれるということを実感しました。

早くコード書き始めたいのに 1 ヶ月のうち 1 週間も issue で議論し続けていて、開発が進まない (ように見えた) ので本当に大丈夫か? という焦りもありましたが、やっておいてよかった作業でした。おかげでその後は設計を修正せずとも開発を進められました。

設計 issue が完了し喜んでいる図

https://gyazo.com/306b5c526edadffbfcf66dba7d142084

[Good / Keep] Faraday

検索結果ページの取得・パースの実装に Faraday を使いました。また Faraday に切り替えるついで、コードを綺麗にリファクタリングしました。アルバイト期間終了 3 日前になって既存のコードを捨ててリファクタリングするのは勇気がいりましたが、綺麗なコードを残してから帰ることができたのは良かったと思います。

プルリクコメントでこう言ってもらえたのが嬉しかったです。

https://gyazo.com/588446ee220867c6bf4e7c4df02cb81f

Faraday は、リクエスト・レスポンスの間にミドルウェアを差し込み、それぞれの処理をフックできます。これを利用して、広告をパースして結果 (Hash) をレスポンスボディとして書き換えるミドルウェアを作りました。ミドルウェアによって、「リクエスト→レスポンス→HTMLパース→広告データ」の変換処理を綺麗にワンストップで実装できました。

また、Faraday は HTTP クライアントのバックエンドに共通のインタフェースを持っていて、アダプタとしてバックエンドを差し替えることが出来ます。なので、例えば「Net::HTTP じゃパフォーマンスでないから Typhoues に乗り換えたい」となったとき、リクエスト・レスポンス周りをほとんど書き換えることなく、別のバックエンドに乗り換えることができます。今回は、本番環境のメモリが少なかったので、必要に応じてバックエンドを切り替えてチューニングできる Faraday を選びました。

github.com

[Problem] 思ったより本番環境のメモリ容量が小さく、アルバイト期間終了までに指定のハードウェアでクローラが動かなかった

社内ツールなので僕の手を離れたあとも開発が続けられるわけですが、これが今回のアルバイトで一番の心残りです。

メモリ使用量を削減するために、Capybara → Mechanize → Faraday + Nokogiri と何度も技術選定を変えており、そのたびに発生するコードの書き直しやプロファイルに時間がかかりました。

[Try] 本番環境の要件を事前に調べる、技術選定時にプロファイラを使う

確証もないのにちゃんと動くと思い込んで、本番環境を考慮した技術選定を行わなかったのが原因だと思います。本番環境のメモリ容量を事前に調べ、それを基準に技術選定をしていれば技術選定にここまで右往左往することはなかった気がします。

[Try] リプレイス案件のときは KPI を設定して、既存のツールからの改善具合を計る

今回は、リプレイスにあたって具体的な目標値を設定出来ていませんでした。クロールの速度やメモリ消費量などの目標を設定できていれば、それを考慮に入れた早めの施策を実行できただろうし、単なるリプレイス以上の価値を出すことができたように思います。目標を設定していれば、例えば「既存クローラの性能がxxで、新しいクローラの性能がyyだから 2 倍の高速化を実現した」ということを言えたのになと。


まとめ

学んだこと

  • DB 設計に工数をかけしっかり固めることで、後の開発がスムーズになる
  • 本番環境を考慮に入れて技術選定をすること、確証なく動くだろうと思わないこと
  • レビューは励みになる
  • Faraday

感想

初めてやることが多く、とてもワクワクする開発で、多くの経験値を増やすことができました。処理時間短縮のために Sidekiq で並列にクロール処理させたり、検索エンジンに負荷をかけないようにリクエストを投げたり、DB 設計に一週間かけたり… とどれも初めての経験でした。開発中は細かくレビューを頂くことができ、経験値不足の自分にとって大変有益でした。レビューがあったからここまで何とかやってこれたと思うし、思う存分暴れ自走させてもらうことができて楽しかったです。

Speee の方々へ: 1 ヶ月間ありがとうございました。入社後またよろしくお願いします!


あとは研究だ。