読者です 読者をやめる 読者になる 読者になる

Septeni Engineer's Blog

セプテーニエンジニアが綴る技術ブログ

Akka Streamでスクレイピング時にサーバーに思いやりを与える

Akka Scala

こんにちは、菅野です。

近頃はWeb上で様々なサービスが誕生し、誰もが便利に使っていると思います。また、IFTTTでそれらを繋いだり、イベントをLINEに通知したりするのも簡単です。
それはAPIが公開されているからなのですが、データがほしいけどAPIが公開されていない場合はスクレイピングで自力でデータを取ってくるしかありません。

人力ではなくてプログラムでWebサイトにアクセスするときは、気をつけないとスクレイピング先のサーバーに思わぬ負荷をかけてしまうかも知れません。
早朝に自宅に警察が押し寄せてきて逮捕されてしまうのは避けたいので、サーバをいたわるようにスクレイピングしたいと思います。

Throttleで流量制限

という訳でAkka Streamでさくっと作りたいと思います。
せっかくなのでHTTPクライアントはAkka HTTPを使います。

材料は以下です。依存ライブラリに追加しましょう。

続きを読む

isucon6予選をscala実装で挑戦

新卒1年目の東です。

個人的にハッカソンやコンテストに出るのが好きで、去年はisucon6予選にも挑戦しました。 結果は惨敗だったのですが、isucon6予選はアプリケーションにボトルネックが仕掛けられており、非常に勉強になるものでした。

isucon6予選の言語比率をながめてみると、scala2組だけです。(自分もチームでruby使いましたが)
isuconにscalaで取り組む人への手助けになればと思い、scalaでisucon6予選を初期状態から少し改善するまでを記事にしてみます。

isucon6予選環境の構築 ~ scalaの初期スコア計測まで

vagrantで初期環境構築

これまでのisucon問題をvagrantで構築できるようまとめてくださっています。 本来はazureでVMインスタンスを用意し、ansibleを流して構築する必要があるのですが、今回はこちらを利用して進めていきます。

matsuu/vagrant-isucon
https://github.com/matsuu/vagrant-isucon

今回はisucon6-qualifier-standaloneを利用します。

計測用ツールの導入

isuconといえばスロークエリやアクセスログの解析、プロファイラを利用したりCPU・メモリ状況を観測してボトルネックを発見する、という部分に結構面白さがあります。 このツール群については既に色々な方が触れていると思うので、参考程度に自分が利用しているツールを挙げておきます。

pt-query-digest: スロークエリ解析
https://www.percona.com/doc/percona-toolkit/2.2/pt-query-digest.html

alp: アクセスログ解析
https://github.com/tkuchiki/alp

その他、vmstat・sysstat・dstat・topなどを適宜

scala実装への切り替えとスコア計測

今回の初期実装はperlで動作しており、systemdで管理されています。 scala実装に切り替えた上で初期スコアを計測してみます。

$ systemctl disable isuda.perl.service isutar.perl.service
$ systemctl enable isuda.scala.service isutar.scala.service

今回はisuda, isutarという2種類のサービスによって構成されているようです。 一旦vagrant reloadをかけた上で、ベンチマークをかけてみます。

$ ./isucon6q-bench -target http://127.0.0.1
2017/01/03 12:07:57 start pre-checking
2017/01/03 12:08:20 pre-check finished and start main benchmarking
2017/01/03 12:08:57 benchmarking finished
{"pass":true,"score":0,"success":121,"fail":57,"messages":["リクエストがタイムアウトしました (GET /)","リクエストがタイムアウトしました (GET /keyword/2089年)","リクエストがタイムアウトしました (GET /keyword/684年)","リクエストがタイムアウトしました (GET /keyword/イギリス政府)","リクエストがタイムアウトしました (GET /keyword/ウーズ)","リクエストがタイムアウトしました (GET /keyword/ジェイアールバス東北大湊営業所)","リクエストがタイムアウトしました (GET /keyword/ナンバーディスプレイ)","リクエストがタイムアウトしました (GET /keyword/フーリエ)","リクエストがタイムアウトしました (GET /keyword/印旛村)","リクエストがタイムアウトしました (GET /keyword/巨大基数)","リクエストがタイムアウトしました (GET /keyword/神戸国際会館)","リクエストがタイムアウトしました (POST /keyword)","リクエストがタイムアウトしました (POST /login)"]}

初期スコアは0のようです。めげずに改善していきましょう。

ボトルネックを改善し、スコア0から脱出

ボトルネックを特定する準備

ベンチマーク実行中のtopを眺めてみます。

 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
2607 isucon    20   0 2990956 504256  19876 S 169.7 24.6   3:40.39 java
2606 isucon    20   0 2942064 274508  18500 S  52.2 13.4   0:40.19 java
1282 isucon    20   0  118136  10372   5752 S   2.3  0.5   0:02.61 isupam
2793 root      20   0  139580  87804   7184 S   1.0  4.3   0:01.60 isucon6q-bench
1348 mysql     20   0 1543880 230616  16944 S   0.7 11.3   0:05.30 mysqld

topの時点ではざっくりとしかわからないのですが、javaプロセスのCPU使用率が高いことからアプリケーションにボトルネックがあることが推測されます。

次にalpを用いてアクセス解析を行っていきます。

$ alp -f /var/log/access.log --aggregates "/keyword/*" -r
+-------+-------+--------+---------+--------+-------+--------+--------+--------+------------+------------+-------------+------------+--------+-----------------------------------+
| COUNT |  MIN  |  MAX   |   SUM   |  AVG   |  P1   |  P50   |  P99   | STDDEV | MIN(BODY)  | MAX(BODY)  |  SUM(BODY)  | AVG(BODY)  | METHOD |                URI                |
+-------+-------+--------+---------+--------+-------+--------+--------+--------+------------+------------+-------------+------------+--------+-----------------------------------+
|    18 | 1.421 | 15.005 | 187.433 | 10.413 | 1.421 | 15.000 | 15.005 |  6.163 |      0.000 |  44556.000 |   94325.000 |   5240.278 | GET    | /keyword/*                        |
|    21 | 2.895 | 15.005 | 265.020 | 12.620 | 2.895 | 15.000 | 15.005 |  3.679 |      0.000 |  80358.000 |  523460.000 |  24926.667 | GET    | /                                 |
|    71 | 0.139 |  3.006 | 167.744 |  2.363 | 0.139 |  3.000 |  3.005 |  1.065 |      0.000 |      0.000 |       0.000 |      0.000 | POST   | /login                            |
|    11 | 0.473 |  3.002 |  18.487 |  1.681 | 0.473 |  0.769 |  3.002 |  1.124 |      0.000 |      5.000 |      10.000 |      0.909 | POST   | /keyword/*                        |
|    19 | 0.207 |  0.616 |   7.287 |  0.384 | 0.207 |  0.321 |  0.606 |  0.139 | 106015.000 | 106015.000 | 2014285.000 | 106015.000 | GET    | /css/bootstrap.min.css            |
|    10 | 0.115 |  0.600 |   3.258 |  0.326 | 0.115 |  0.289 |  0.445 |  0.145 |  16849.000 |  16849.000 |  168490.000 |  16849.000 | GET    | /css/bootstrap-responsive.min.css |
|    10 | 0.151 |  0.583 |   3.504 |  0.350 | 0.151 |  0.311 |  0.546 |  0.132 |  86351.000 |  86351.000 |  863510.000 |  86351.000 | GET    | /js/jquery.min.js                 |
|    10 | 0.212 |  0.558 |   3.429 |  0.343 | 0.212 |  0.319 |  0.437 |  0.102 |  28631.000 |  28631.000 |  286310.000 |  28631.000 | GET    | /js/bootstrap.min.js              |
|    10 | 0.109 |  0.548 |   3.361 |  0.336 | 0.109 |  0.285 |  0.547 |  0.141 |   1092.000 |   1092.000 |   10920.000 |   1092.000 | GET    | /favicon.ico                      |
|    10 | 0.195 |  0.536 |   3.350 |  0.335 | 0.195 |  0.296 |  0.465 |  0.109 |     93.000 |     93.000 |     930.000 |     93.000 | GET    | /img/star.gif                     |
|     1 | 0.330 |  0.330 |   0.330 |  0.330 | 0.330 |  0.330 |  0.330 |  0.000 |     79.000 |     79.000 |      79.000 |     79.000 | GET    | /css/main.css                     |
|     1 | 0.213 |  0.213 |   0.213 |  0.213 | 0.213 |  0.213 |  0.213 |  0.000 |    695.000 |    695.000 |     695.000 |    695.000 | GET    | /js/star.js                       |
|     1 | 0.131 |  0.131 |   0.131 |  0.131 | 0.131 |  0.131 |  0.131 |  0.000 |      0.000 |      0.000 |       0.000 |      0.000 | GET    | /logout                           |
|     1 | 0.121 |  0.121 |   0.121 |  0.121 | 0.121 |  0.121 |  0.121 |  0.000 |     15.000 |     15.000 |      15.000 |     15.000 | GET    | /initialize                       |
+-------+-------+--------+---------+--------+-------+--------+--------+--------+------------+------------+-------------+------------+--------+-----------------------------------+

どうやら//keyword/*に時間がかかっているようです。

ボトルネックを特定する

では、実際にコードをみていきます。 上記までの流れで、アプリケーションのエンドポイント2つにボトルネックがある予想を立てているので、コード内に散見するその他の部分については考えずに読んでみます。

get("/")(withUserName { maybeUserName =>
  /**
   * ページング処理やDBからのデータ取得
  */
  render("index",
    "user" -> maybeUserName,
    "entries" -> entries.map(e => Map("entry" -> e.toHash)),
    "pages" -> pages.map(p => Map(
      "page" -> p,
      "active" -> (p == page)
    )),
    "prev" -> Option(page - 1).filter(_ > 0),
    "next" -> Option(page + 1).filter(_ < lastPage)
    )
  }
)

get("/keyword/:keyword")(withUserName { maybeUserName =>
  val result = for {
    /**
     * DBからのデータ取得
    */
  } yield render("keyword",
    "user" -> maybeUserName,
    "entry" -> entry.toHash
  )
  result.merge
)

上記の処理について、気になるのは標準ライブラリの処理ではない.toHashの部分です。

implicit class EntryOps(private val entry: Model.Entry) extends AnyVal {
  def toHash: Map[String, Any] =
    toJSON(entry).extract[Map[String, Any]] ++ Map(
      "url" -> s"/keyword/${entry.keyword.uriEncoded}",
      "html" -> htmlify(entry.description),
      "stars" -> loadStars(entry.keyword).map(s => Map("star" -> s))
    )
}

.uriEncoded, htmlify(), loadStars()が改善対象になりそうです。

これらについて改善の優先度をつけるには、プロファイラを用いて処理時間の測定を行う必要があるのですが、プロファイラの導入がうまくいかなかったので...今回は保留にします。

isucon6予選経験者の方ならご存知だと思いますが、今回はhtmlify()をどう改善するかというのが肝になっていました。今回はhtmlify()を改善してみます。

ボトルネックを改善する

htmlify()の内容は以下の通りです。

def htmlify(content: String): String = {
  val entries = DB.readOnly { implicit session =>
    sql"""
      SELECT * FROM entry
      ORDER BY CHARACTER_LENGTH(keyword) DESC
    """.map(asEntry).list.apply()
  }
  val regex =
    entries.map(e => Pattern.quote(e.keyword)).mkString("(", "|", ")").r
  val hashBuilder = Map.newBuilder[String, String]
  val escaped = regex.replaceAllIn(content, m => {
  val kw = m.group(1)
  val hash = s"isuda_${sha1Hex(kw)}"
  hashBuilder += kw -> hash
    hash
  }).htmlEscaped
  hashBuilder.result.foldLeft(escaped) { case (content, (kw, hash)) =>
    val url = s"/keyword/${kw.uriEncoded}"
    val link = s"""<a href="$url">${kw.htmlEscaped}</a>"""
    content.replaceAllLiterally(hash, link)
  }.replaceAllLiterally("\n", "<br />\n")
}

htmlify()の処理は、与えられた文字列の中にDBに登録済みのキーワードがあった場合に、そのキーワードのページへのリンクを生成・置換する内容になっています。

この処理の問題点を考えてみます。

  1. 処理の都度、DBのentryテーブルから全取得して正規表現を生成している
  2. 正規表現にマッチするキーワードの抽出とリンクへの置き換えを別々の反復で行っている

1について、メモリ上に載せてしまうのが良さそうです。scalaにはシングルトンを保証するobjectが用意されているので、以下のようなEntryRegexオブジェクトを実装します。

object EntryRegex {
  import Model.WrappedSetConvert

  private var value: String = ""

  def initialize(): Unit = {
    val entries = DB.readOnly { implicit session =>
      sql"""
        SELECT * FROM entry
      """.map(_.toEntry).list.apply()
    }
    value =
      entries.map(e => Pattern.quote(e.keyword)).mkString("(", "|", ")")
  }

  def regex: Regex = value.r
}

キーワードの追加・削除・更新があった時にどうするか?という問題が出てきますが、既存のDBへ処理をそのままに、都度initialize()を実行することにします。 並列処理の時どうするのかとか考慮せずobjectvarを使ってたり、実務でやったら首をはねられそうなコードになってますが... isuconなので時間もないですし、問題が起きてから考えればいいです。

1,2の改善後のhtmlify()は以下の通りです。

def htmlify(content: String): String = {
  val regex = EntryRegex.regex
  val hashBuilder = Map.newBuilder[String, String]
  regex.replaceAllIn(content.htmlEscaped, m => {
    val kw = m.group(1)
    val url = s"/keyword/${kw.uriEncoded}"
    val link = s"""<a href="$url">${kw.htmlEscaped}</a>"""
    link
  }).replaceAllLiterally("\n", "<br />\n")
}

だいぶすっきりした気がしますが、実際の処理は改善されたでしょうか? 再度、計測してみます。

$ ./isucon6q-bench -target http://127.0.0.1
2017/01/03 16:28:45 start pre-checking
2017/01/03 16:28:52 pre-check finished and start main benchmarking
2017/01/03 16:29:45 benchmarking finished
{"pass":true,"score":3154,"success":1198,"fail":1,"messages":["荒川道路 に 中部地方の道路一覧 へのリンクがありません (GET /)"]}

エラーが発生しているようですが、無事スコア0から脱出できました。 本番のインスタンスvagrant環境より性能が良いので、この内容でももっとスコア出るんじゃないかと思います。

感想

2015年, 2016年とisuconに挑戦してきましたが、ベンチマークを回すまでミスに気がつきにくいのが辛い部分でした。(意図しないnullの混入など)

特に本番はベンチマークを外部から実行する形式のため、長いベンチマーク実行待ちの後にエラーが吐き出されるとそれだけで気力が吸われます。 単体テストが用意されていないisuconに対して、コンパイル時にある程度ミスを発見しつつガシガシ書けるのは嬉しさがあります。

2017年のisuconはscalaで決勝進出者が出るといいなあ。

とある技術者の生活習慣

f:id:y_harada:20161227171305p:plain
http://to-a.ru/様より

0. はじめに

皆様はじめまして、エンジニアの原田です。
題名は出落ちなので、無視してください。
今回は毛色を変えて、弊社の勤怠制度のお話をしたいと思います。

弊社では「裁量労働制」をとっており、簡単にいうと好きな時間に働いて、好きな時間に帰ることができます。
(一部、この時間は必ずでてくださいといったMTGがある場合はあります。)

1.裁量労働とは?

労働者は実際の労働時間とは関係なく、労使であらかじめ定めた時間働いたものとみなされる
といった制度です。
つまり、
会社に5分でも出社していれば1日いる扱いになる
逆にいえば20時間働いても8時間労働と同じである
といったものです。

これだと、好きに働けるという毛色が強いのですが、一応許されないこともあります。
1つは無断の休日出勤です。
これは、労務上別扱いになるため、駄目だそうです。やったことないですが…

2. 一般ケースエンジニアMの例

典型的(多数派)のワークスタイルです。
ほぼほぼ定時制とかわりませんが、たまに午後からある勉強会のために早退もすることが可能です。

<通常時>
09:00 起床
10:00 出社
17:00 夕会(必須参加のMTG
19:00 退社
<私事>
25:00 就寝

<社内勉強会シフト>
08:00 起床
09:00 社内勉強会予習
10:00 出社
17:00 夕会(必須参加のMTG
17:30 夕会終了
18:30 社内勉強会予習
19:00 社内勉強会開始
21:00 退社
<私事>
24:00 就寝

3. 朝型エンジニアTの例

朝はやくきてさっさと夕活がしたいエンジニアのTさんの例です。
彼の一言「 夕会もう少し早くして早く帰りたいです」のお陰で夕会が17:00になりました。
はっきり言って僕には真似ができません。

06:30 起床
08:30 出社
17:00 夕会(必須参加のMTG
17:30 夕会終了&退社
<私事>
23:00 就寝

4. 夜型エンジニアHの例

仕事は最短で終わらせると意気込むHさんの例です。
しっかりとスプリントでコミットしたチケットさえこなせていればこういった働き方でも問題ありません。

11:30 起床
<私事>
15:00 出社
17:00 夕会(必須参加のMTG
17:30 夕会終了
20:00 退社
<私事>
27:00 就寝

5. エンジニアリーダーKの例(別チーム)

自チームだけ特殊か?というわけで別チームにもヒアリングに行きました。
リーダーが率先して裁量労働をうまく使っているので萎縮して裁量労働を使えないということはありません。

10:30 起床
11:45 出社
20:00 退社
<私事>
27:00 就寝

このような出社スタイルをとっており、味噌は「MTGは午後からいれてくれ」だそうです。

6. まとめ

上記の例のとおり、弊社では様々なライフスタイルに対応した働き方ができます。
弊社では、仕事だけではなくライフスタイルの「充実」に対しても取り組んでおります。

羨ましいなと思った方、ぜひ一緒に働きませんか?

株式会社セプテーニ・オリジナル

セプテーニグループ採用サイト | Septeni Group Mid Career 中途採用

Akka HTTP で LINE bot を作ってみました

Akka Akka HTTP Scala bot

こんにちは、Xiaoです。

最近Akka HTTPの正式版 X がで出ましたね。 自分は前からAkka HTTPに興味があって、ちょうどいい機会なので、Akka HTTPを詳しく勉強するため、簡単のWebサービスを作ってみたいと思います。

LINE Bot とは

LINE が提供する Messaging API により、LINEのトーク画面を使った対話型Botアプリケーションの開発が可能になります。

図

※ Image taken from developers.line.me

この図を説明すると、

  • サーバのURLを LINE Bot にするアカウントと繋ぎます。
  • ユーザがBotに対して、友たち追加、メッセージ送信などを行う際に、設定されたサーバにイベント情報が送られます。
  • サーバがイベント情報を基づいて、LINE Messaging API経由でユーザに返信できます。

※ LINE アカウントの設定について、LINEの公式ドキュメントに記載されているので、割愛します。

Akka HTTPについて

Akka HTTP は Akka Actor と Akka Stream をベースに作られた、HTTPサーバ/クライエント処理をサポートするためのツール、ライブラリです(フレームワークではありません!)。

LINE botを作るには、Akka HTTP みたいなツールが丁度良いと思います。

今回で作る Bot

今回は Akka HTTP の特性の紹介するため、一番シンプルな「受けたメッセージをそのまま返す」Botを例にして、Akka HTTP の特性を幾つか紹介したいと思います。

続きを読む

セプテーニ・オリジナルの開発環境・福利厚生について

こんにちは、10月入社した盛岡です。 とても快適な環境で開発できているので、紹介致します。

開発環境

1. PC・周辺機器

  • MacBookPro 15インチ(CPU:2.3GHz i7, メモリ:16GB, SSD:512GB)
  • キーボード、マウス、追加モニター等業務に必要な物は会社負担で購入可能

ぼくのかんがえたさいきょうのPCまわり f:id:morizo999:20161213155806j:plain:w600

  • キーボード:REALFORCE108UG-HiPro
  • マウス:SteelSeries Rival 300
  • モニター:DELL U2414H * 2

2. 開発ツール

  • IDEIntelliJ IDEA(入社した次の日にライセンスがもらえた!)
  • コード管理はBitBucket
  • ChatはSlack
  • チケット管理はJIRA
  • CIはBamboo

3. 裁量労働制

裁量労働についてはドワンゴさんでも話題になっていましたが、セプテーニ・オリジナル(以下、セプオリ)も導入しております。午前休・午後休という概念は存在せず、結果を出している限り、業務時間は問われません。
セプオリも自由度が高いので、セプオリの呪いにかかってしまうかもしれません。 nalgami.hateblo.jp

4. 中途研修

入社して一番驚いたのが、中途入社で研修がありました。

  1. TDD研修(Scalaの文法に慣れる)
  2. 掲示板作成(Play Frameworkになれる)
  3. DDDで掲示板再作成(ドメイン駆動に触れる)

上記の流れで研修を受けました。Scalaの経験はなかったのですが、丁寧なレビューを受けてとても勉強になりました。 DDD研修も色々な罠があり、しなやかな設計は難しいと実感できました。

福利厚生

1. 社内カフェ

オフィスのすぐ横に社内カフェがあります。
注文後にジューサーでとても美味しいフレッシュジュースを作ってくれるので毎日飲んでます。 dimo.jp

2. お菓子神社

色んなお菓子が用意されており、食べた分料金を入れる形で運用しています。
日々お菓子が変わっていくので、見ている分にもとても楽しいです。 f:id:morizo999:20161213155845j:plain:w600

3. ウォーターサーバー

全社で使えるカフェにも置いてあるのですが、セプオリ単体でもウォーターサーバーがあり、飲み放題です。
痛風持ちの僕としてはとても嬉しい福利厚生です。

4. 生活雑貨

会社生活に必要な雑貨も会社で用意してもらえます。
ティッシュ・ウェットティッシュ・マスク・付箋・ボールペン等色々あります。
自由にティッシュと戯れられる会社です。

また、最近湿度が低くて加湿器あったほうがいいのではという議論があったのですが、
一週間後にコイツが来ました。 f:id:morizo999:20161213161117j:plain:w600

5. カンファレンス参加制度

スプリントに問題なければ、平日でも業務&経費としてカンファレンスに参加することができます。

まとめ

入社してからまだ2ヶ月でも色々改善が目に見えるので、とても居心地がいいです。
というわけでエンジニア募集してますのでよろしくお願いします! www.septeni-holdings.co.jp

SPAじゃなくてもWebpack導入(ファイル監視&自動リロード + レガシーブラウザ対応 + JS最適化 + ES2015(ES6) + SCSS + ESLint)

Septeni Originalの大久保です。

image

世間ではSPAが流行ってるかと思います。

ただHTMLのレンダリングはサーバー側で行うケースは以前多いのではないでしょうか?

現在自分が携わってる新規プロダクトでモダンなフロントエンドの技術(WebapackやES2015など)を使いつつ、HTMLレンダリングはサーバーに委ねるようなアーキテクチャ(Multi Page Application)の調査を行ったので、そこでの知識を備忘録的にまとめたいと思います。

今回作成したコードはGithubにあげているので、興味あればご参照ください。 Github URL:https://github.com/yoppe/webpack-es2015-base-for-play

続きを読む