Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

インフラエンジニアがオフィス移転で考えたこと[後編]

インフラエンジニアの菅原です。

弊社のオフィス移転が完了しました。
f:id:ksugahara08:20190111111915j:plain

前編で引越しの計画を立てたものの、事は上手く運ばず、色々学びがありました。

今回はインフラエンジニアがオフィス移転で考えたこと[前編]の続きとして振り返りと学びを共有したいと思います。

プロバイダー契約

振り返り

現状の棚卸しをしたところインターネット回線の2重契約や電話回線の契約に不要なFAXの契約が入っていたりしていたため、契約プランを変更することでコストカットを進めました。

また、前編でも話した通り社内ネットワークの平行期間を設けました。具体的には以下の様にインターネット回線契約を「解約」と「新規」にしました。
「解約」・・・旧オフィスを12月末まで
「新規」・・・新オフィスを12月頭から
平行期間を設けることで余裕を持って動作確認を行うことが出来ました。
しかし、全て上手くいったわけではありませんでした。プロバイダーとの認識のズレで旧オフィスのインターネット回線を「移行」にさせられてしまい、12月頭にインターネットが使えないという事態も起きました。

学び

棚卸しはコストカットに繋がることもありますが、契約の変更はよく検討した方が良いと思います。電話回線契約が必要だったりと、要件を見極めるためにも前任者に連絡が取れるのなら事情を確認してからにした方が良いと思います。

社内ネットワークの移行は平行期間を設けたことで余裕が生まれました。
しかし、インターネットが旧オフィスで使えない期間ができてしまいました。プロバイダーには旧オフィスの解約日を書面等で明確に伝えておくべきだと思います。

AWSの設定変更

振り返り

弊社では社内の固定グローバルIPでアクセス制御をしている部分があります。
移転時には固定IPが変更になってしまうため、各サーバーに設定変更をかける必要がありました。

具体的にはSecurity Group、IAM、nginxの設定箇所を移転に合わせて変更しました。
事前に移転先の固定IPがプロバイダーから発行されるので、引越し前に新固定IPを追加し、引越し後に旧固定IPを削除する形を取りました。

学び

前任者の方が移転のことも鑑みてAnsibleで設定をまとめてあったので非常に楽でした。
構成管理ツールでサーバーの設定を記述しておくことはオフィス引越しでもメリットがあります。

コミュニケーション

振り返り

今回の移転で最もコストが掛かったのは、関係各所とのコミュニケーションでした。話したことと実際のアウトプットが違っていたり、合意した内容に齟齬が発生していました。
ドキュメントを作って満足するのではなく、相手が理解・納得して合意を取る。進捗確認・共有という基本的な部分が当初から出来ていなかったのは反省点だと思います。

学び

闇雲にミーティングを行うのは愚策だと思いますが、話した事や決まった事はメールやドキュメントで書類にして合意を形成するという基本をちゃんと行っておくべきだと思います。基本大事!!

スケジュール

振り返り

実際に作業して頂く先方とスケジュールを確認したところチグハグになっていることがわかりました。
コミュニケーション不良もありましたが、スケジュールが明確になっていない、進捗がわからないという状況になりかけていました。

学び

自分兼共有用のスケジュールやチェックリストを作成して、共有するようにしました。
具体的には以下のようなものを用意しました。

日付 担当 内容 場所 ステータス
11/15 電話回線業者 電話回線工事(立会) 御茶ノ水
11/21 ISP業者 光回線工事(ダークファイバー側)(立会) 御茶ノ水
11/27 ISP業者 光回線工事(1階から4階側)(立会) 御茶ノ水
12/3 社内インフラ 新固定IPをAnsibleで追加する 代々木
12/4 電話回線業者 電話回線本工事(立会) 御茶ノ水
12/4,5 電気工事業者 無線AP配線取付、新規ルーター設置 ネットワーク試験 電話先行配線(立会) 御茶ノ水
12/14 社内インフラ 床下配線回収、複合機の持ち出し 代々木
12/15 電気工事業者 2台目ルーター設定、動作確認、電話設備撤去運搬取付、複合機用LAN増設 御茶ノ水
12/15 複合機業者 複合機の移設、設定 御茶ノ水
12/15 電話回線業者 旧モデム一式返却(13時〜17時立会必要) 代々木
12/15 社内インフラ 床下配線回収 代々木
12/17 複合機業者 2台目複合機の増設、設定 御茶ノ水
12/17 社内インフラ 旧固定IPをAnsibleで削除する 御茶ノ水
12/19 ISP業者 旧モデム一式返却(15時頃) 代々木
12/20 複合機業者 2台目複合機の増設、設定 御茶ノ水

全体から逆算して、いつまでに何が出来ていないといけないか把握することは大切だと改めて思いました。

移転作業

振り返り

旧オフィスでは床下の配線を弊社側で行っていたため原状回復が必要でした。ビル側の要望としても元に戻す様に言われていたため配線を回収する必要がありました。

f:id:ksugahara08:20190111114139j:plain
床下の配線回収

学び

原状回復でどこまで行うかはビルの管理を行っている方に確認したが良いと思います。有志を募って回収作業を行いましたが、回収方法などの計画は事前に練っておいた方が良いと思います。

まとめ

  • プロバイダー契約は棚卸しして、ゼロベースで検討してみても良いと思います。
  • 移転時はインターネット回線の平行期間を設けると色々とメリットがありました。
  • コミュニケーション不足陥らないためにスケジュール確認や進捗確認などまめに行うべきだと思います。エビデンスは文面で残す様にしましょう。
  • 移転作業における自分の作業範囲を確認しましょう。
御茶ノ水新オフィス紹介

新オフィスにはイベントスペースやバーカウンター、自由に使える作業スペースもあります。

f:id:ksugahara08:20190111112818j:plain
イベントスペース
f:id:ksugahara08:20190111112659j:plain
バーカウンター
f:id:ksugahara08:20190108105522j:plain
作業スペース

弊社では新しいオフィスで働く仲間を募集しております。
興味のある方はこちらからご連絡下さい。

Railsで流量の多いデータをページネーションする

はじめまして、for School事業部のサーバーサイドエンジニアの冨山です。
今回はfor SchoolのリニューアルにおいてAPIでリアルタイムなデータをページネーションする上でJinraiというカーソルベースのページネーションライブラリを開発しました。
今回はその開発の経緯や直面した課題についてお話していきたいと思います。

TL:DR

  • ページベースのページネーションだとクライアントへ渡すデータに重複が発生する。
  • 基点となるレコードを示すカーソルをクライアントへ渡すことでOFFSETを意識することなく特定の範囲のデータを取得できる。
  • IDをクライアントに渡したくない場合、カーソルとして渡す値がユニークで無いとクライアントに渡らない情報が発生する。
  • ソートキーの値が重複する場合IDでもソートすることで上の問題が解決する

Jinraiの使い方

class Post < ApplicationRecord
  cursor_per 5 # 一度に取得する件数の指定
end

class PostsController < ApplicationController
  def index
    @posts = Post.cursor(since: params[:since], till: params[:till])
  end
end

posts = Post.cursor # 最新5件のレコード
since = posts.till_cursor # postsの最後のレコードを示すカーソル
Post.cursor(since: since) # postsの最後のレコード以前の5件のレコード

till = posts.since_cursor # postsの最初のレコードを示すカーソル
Post.cursor(till: till) # postsのレコードより最新のレコード

デフォルトではidでソートされますが、id以外のキーでソートしたい場合sort_atオプションを渡すことで実現できます。

Post.cursor(sort_at: :updated_at) #=> updated_atでソートされた上から5件のレコード

開発経緯

Studyplus for Schoolでは生徒の勉強記録の情報をタイムラインに表示するような機能があります。

当初はKaminariを使っていましたがそれだとデータの流量が増えたときにページ間でデータの重複が発生するためカーソルベースのページネーションを作ろうという話になりました。

課題1

最新データから一定件数ずつ取得するタイムラインでは常にデータの先頭に最新のレコードが挿入され続けるためページ型のページネーションだとデータの重複が発生します。
以下にその一例を示しますが、下のような投稿のテーブルがあるとします。

posts

id text updated_at
10 post10 2018/12/13 14:12
9 post09 2018/12/13 14:11
8 post08 2018/12/13 14:10
7 post07 2018/12/13 14:09
6 post06 2018/12/13 14:08
5 post05 2018/12/13 14:08
4 post04 2018/12/13 14:06
3 post03 2018/12/13 14:05
2 post02 2018/12/13 14:04
1 post01 2018/12/13 14:03

Kaminariで上のテーブルから最新のデータを5件づつデータを取得すると、

Post.order(id: :desc).page(1).per(5).pluck(:id) #=> [10, 9, 8, 7, 6]

発行されているSQLは

SELECT posts.* FROM posts ORDER BY posts.id DESC LIMIT 5 OFFSET 0

となります。

ここで新しい投稿があり、データ更新されるとテーブル内の情報は下のように更新されます。

posts

id text created_at
11 post11 2018/12/13 14:20
10 post10 2018/12/13 14:12
9 post09 2018/12/13 14:11
8 post08 2018/12/13 14:10
7 post07 2018/12/13 14:09
6 post06 2018/12/13 14:08
5 post05 2018/12/13 14:08
4 post04 2018/12/13 14:06
3 post03 2018/12/13 14:05
2 post02 2018/12/13 14:04
1 post01 2018/12/13 14:03

このタイミングで先程の方法で2ページ目を取得すると、発行されるSQLは

SELECT posts.* FROM posts ORDER BY posts.id DESC LIMIT 5 OFFSET 5

となるため

Post.order(id: :desc)page(2).per(5).pluck(:id) #=> [6, 5, 4, 3, 2]

は、id=6のデータが重複してしまいます。 ページベースのページネーションで上のようなリアルタイムデータを扱うと重複や削除処理などが入れば表示されないデータが発生します。

解決策

そこで最初と最後のレコードを一意に特定可能な値を渡すことで、そのレコードを基点に取得すれば上のような問題は発生しなくなります。 一般的な方法にIDを渡す方法があり、IDはユニークなのでクライアントへ直前に渡したデータの任意のIDをリクエストに含めて貰えれば重複なく前後のデータを返すことができます。

例えばkaminariでは2ページ目を取得する場合

curl -X GET http://example.com/users?page=2

といったリクエストを受けますが、Jinraiでは以下のような形でリクエストを受けます。

curl -X GET http://example.com/users?since=6

ここで発行されるSQLは以下のようになります。

SELECT  posts.* FROM posts WHERE posts.id < 6 ORDER BY posts.id DESC LIMIT 5

この方法であれば上の新しい投稿がされた場合であっても直前に受け取ったデータの最後のIDをリクエストのsinceパラメータに含めてあげることで、重複なくデータを取り出すことができます。

課題2

上でデータの重複は解決できますが、クライアントにIDを渡したくかったり、IDでなくupdated_atなどの異なる属性でソートされたデータが欲しいといったケースが出てくると思います。

下はpostsをupdated_at > idの順序でソートされたデータです。

posts

id text updated_at
10 post10 2018/12/13 14:12
9 post09 2018/12/13 14:11
8 post08 2018/12/13 14:10
7 post07 2018/12/13 14:09
5 post06 2018/12/13 14:08
6 post05 2018/12/13 14:08
4 post04 2018/12/13 14:06
3 post03 2018/12/13 14:05
2 post02 2018/12/13 14:04
1 post01 2018/12/13 14:03

こちらのテーブルから最初の5件を取得すると

Post.cursor(sort_at: :updated_at).pluck(:id) #=> [10, 9, 8, 7, 5]

となります。次にその次の5件を上と同じSQL取得すると、

SELECT posts.* FROM posts
WHERE posts.updated_at < '2018/12/13 14:08' ORDER BY posts.updated_at DESC LIMIT 5

という形ですが、直前に取得したデータの最後(id=5)のupdated_atが次のレコード(id=6)のupdated_atと同じ値の為id=6のレコードが取得できなくなってしまいます。

解決策

そこでJinraiではid以外の属性でソートするときは以下のようなSQLを発行します。

SELECT posts.* FROM posts
WHERE (posts.updated_at < '2018/12/13 14:08')
OR ((posts.updated_at = '2018/12/13 14:08') AND (posts.id > 6))
ORDER BY posts.updated_at DESC LIMIT 5

WHERE句でソートキーの値が重複する可能性を考慮して、同じだった場合より大きいidのデータを取得するようにしています。これを実装上でArelを使用してコードで以下のように表現しています。

if sort_at != primary_key
  condition_1 = arel_table[sort_at].send(rank, attributes[sort_at])
  condition_2 = arel_table.grouping(arel_table[sort_at].eq(attributes[sort_at])
    .and(arel_table[primary_key].send(rank_for_primary, id)))
  relation = where(condition_1.or(condition_2))
else
  relation = where(arel_table[primary_key].send(rank, id))
end

まとめ

以上のようにjinraiはページベースのものに比べて、全ページ数などが取得できないといったデメリットはありますが今回のようなケースでは過不足なくデータの受け渡しを行えます。
流量が多く最新からデータを取得したいようなケースに扱いやすい形でデータを提供できるような形で開発されています。

ソースは以下のGithubにあり弊社メンバー以外の方の開発参加も歓迎しています。
OSSの開発に関することはGitterにてやり取りされている為興味がある方は是非一度ルームで声をかけてください。
Github(studyplus/jinrai)
Gitter

Ruby biz Grand prix2018、大賞を受賞しました

こんにちは、CTO島田です。 2018年12月13日、Ruby biz Grand prix2018の表彰式が開催され、スタディプラスが大賞を受賞いたしました!
まさか大賞を頂けるとは思っていませんでしたので、とても嬉しいです。

f:id:kana_peko:20181213161821p:plain:w400 f:id:kana_peko:20181213161912p:plain:w400

今回、Ruby biz Grand prix応募にあたり、当社の取り組みやRubyの活用方法を応募用紙にしたためました。
この開発者ブログをご覧いただいている方に当社についてより詳しく知っていただきたく、コンフィデンシャルな部分を除いて部分的にこちらに公開します。

スタディプラスについてご興味をお持ちの方、また次回以降のRuby biz Grand prixへエントリーを検討されている方にとって、学びにしていただけますと幸いです。

募集要項(一部)

1. 応募団体の概要

(略)

2. 商品・サービス

(1) 商品・サービスの名称等

  • 商品・サービス名
    • Studyplus
  • 商品・サービスのキャッチフレーズ
    • 学ぶ喜びをすべての人へ

(2)商品・サービスの概要

  • サービス概要
    • Studyplusは、学習管理サービスです。
      勉強している教材のバーコードを、スマートフォンのカメラで読み取るだけで、勉強している教材としてスマホに登録し、勉強記録をつけることができます(オリジナルの教材も登録できるため、あらゆる学びを記録することができます)。
      私達は、学習の最大の課題は、学習それ自体ではなく、学習の継続であると考え、Studyplusを開発しました。学習はその効果をすぐには体感しづらいため、日々の学習をStudyplusで記録することで、日々成長していることを可視化したり、同じ目標に向かって学習している学習者をつなげることで切磋琢磨していることを感じやすいようにしています。
  • 用途
    • 大学受験、高校受験、資格試験、語学習得、読書記録など、あらゆる学習の記録にご利用できます。教育事業者にもサービスの提供を開始し、塾や学校の先生が、Studyplusを利用して、生徒の学習モチベーションを見守り、生徒のモチベーションが下がっている時に、声をかけたり、メッセージを送るような事例もあります。
  • 顧客ターゲット
    • 学習を継続したいユーザー
    • 現在のユーザーの内訳は、中学生が10%、高校生・浪人生が56%、大学生が15%、社会人が19%。
  • 顧客アプローチ方法
    • 2012年のサービス開始以来、広告は一切行わず、学習者同士の口コミで集客してまいりました。友達同士で競い、励まし合う、というコンセプトから、特に、中学生や高校生は使いやすいようです。アプリの評価がAppstoreでは★4.7、GooglePlayでは★4.6と非常に高く、アプリストアで知っていただくケースも多いです。

(3) 商品・サービスの安全性確保のため講じている方策

(略)

3. 事業の成長性と持続性

(1) 新規性・独創性・優位性のPR

①事業概要、新規性

Studyplusの新規性は、先生が「教える」ことではなく、生徒が「続ける」ことをIT化した点です。
教育のIT化、いわゆるEdtechは、対面の授業を映像などデジタルに置き換えるコンテンツを扱うサービスがほとんどでしたが、Studyplusは学習記録をつけるだけの、コンテンツを扱わない、まったく新しいサービスになっています。また、収益モデルについても、学習コンテンツのサービスは、学習コンテンツを購入してから学習をはじめるサービスがほとんどですが、Studyplusは、無料サービスで、広告収入で運営しており、生徒が勉強すればするほど、広告収益が増える、教育業界では他にない収益モデルになっています。

② 独創性・優位性

私達は、対面の授業を映像などデジタルに置き換えるコンテンツを扱うサービスが多い中、学習の最大の課題は、学習それ自体ではなく、学習の継続であると考え、例えば、節約の継続のために、支出を記録する家計簿アプリが必要であるように、学習の継続のために、学習を記録するアプリが必要だと考え、開発しました。

また、家計簿アプリは、その性質上、ユーザーが一人で利用するクローズドなサービスになりますが、Studyplusはユーザーが一人で利用するのではなく、学習記録を公開し、同じ目標を目指すユーザー同士が切磋琢磨する点が特徴的です。結果的に、ユーザー同士が切磋琢磨するネットワーク効果がユーザーに評価され、学習管理SNSとして競合はおらず、教育カテゴリのアプリとして圧倒的ナンバーワンのサービスになっています。また、収益モデルとしても、これまで学習意欲の高い生徒に広告を出稿したかった塾や大学が、日々勉強記録をつける学習意欲の高いStudyplusのユーザーに広告を出稿できることに価値を感じていただいており、塾や大学の生徒募集広告市場でもオンリーワンのサービスになっています。

(2) 市場性・成長性のPR

  • Studyplusの累計利用者数は2018年6月時点で350万人を突破しました。2016年10月にはユーザー数200万人、2017年12月には300万人突破と順調にユーザー数を伸ばしています。全都道府県に満遍なくユーザーが存在し、年齢層も幅広いです。アクティブユーザー数は年々増加を続けており、2018年8月のアクティブユーザー数は昨年比118%に増加しました。また、ユーザーの利用頻度が高いことが特徴であり、ユーザーあたり一日のセッション数(Studyplusを起動する回数)は平均14.2となっています。
    ユーザー数の成長を支えているのは、実際にアプリを利用したユーザーからの高い評価です。「このアプリを使い始めてから勉強に対する意識が変わりました」「みんなと共に頑張っている!と実感できるアプリです」といったポジティブなコメントを日々頂いています。
  • 顧客ニーズに応えるための創意工夫・事業検証・フィードバック
    • 地方出張ユーザーインタビュー
    • 目的:地域間の学習者の違いや地域格差の理解のため、出張ユーザーインタビューを推奨
    • 内容:Studyplus for School導入校にて、Studyplusのインストールから記録・教材登録までの導入サポート。

(3) 将来性のPR

(中略)

  • 学習管理機能に加えて、学習に関する記事コンテンツを読めるなど欲しい情報を受け取れるサービスへ進化
  • SNSとしての機能をより洗練させ、学習に関するさまざまな情報をシェアしやすいコミュニティとなり、学習する人にとって欠かせないサービスを目指す
  • 学習量に加えて習熟度を網羅した学習ビッグデータを活用した学習者へのレコメンデーション機能

4. Rubyの関わり方(優位性のPR)

アーキテクチャ図

f:id:kana_peko:20181213160638p:plain

(1) Rubyの採用理由
  • 「ビジネス上」「技術上」の背景とその理由(採用理由)
    • ネイティブアプリのサーバーサイドAPI開発をする際に、RubyやRailsの安定性や採用事例、情報の豊富さから一定以上のスピードと柔軟性を持った開発が実現できると感じたため。
  • 実際に使って実感したRubyの強み・メリット
    • 簡潔な実装と、高い生産性。
    • ライブラリ(gem)の豊富さによる汎用的な仕組みの再利用性の高さ。
(2) アーキテクチャの難易性等(加点要素)
  • 非同期処理ではSidekiqのenterprise版を利用して、大規模な処理を適切にスケールして実行する仕組みを導入。
  • 複数のマイクロサービスをRuny on Railsを利用して開発。共通化が必要な箇所に社内Gemを利用して実装の統一化を図っています。
(3)システム外での貢献(Rubyの普及啓発)
  • Ruby技術カンファレンスへの協賛
    • RubyKaigi 2018
    • RubyWorld Conference 2018
  • その他Rubyイベントへの協賛
    • 中高生国際Rubyプログラミングコンテスト2018
    • 第10回 Rails Girls Tokyo
  • Rubyコミッターに、設計・コードレビュー等の技術アドバイスを受けています

5. 事業の社会的な影響度(事業の実効性・インパクト)

NPO法人へのStudyplus for School無償提供、経済格差による教育格差の改善を目指しています。

当社は、2018年10月12日(木)より、教育事業者向け学習管理ツールStudyplus for Schoolを、教育格差をはじめとした社会課題解決に取り組む特定非営利活動法人(NPO法人)へ無償提供するプログラムを開始しました。
NPO法人は年々増加傾向にある一方で、経営資源に乏しく、安定した運営が難しいという実態があります。本プログラムは、NPO法人の慢性的な人材不足を、Studyplus for Schoolの活用によって解消し、社会貢献事業を加速させる一助になることを目的としています。
第一弾として、無料塾を運営する「NPO法人キッズドア」へStudyplus for Schoolを無償提供しています。 info.studyplus.co.jp

6. 独自の特色ある事項、アピールポイント

  • 中学生・高校生向け企業訪問プログラム
    • 目的:中学生・高校生の方に、気づきや学びを得る機会を提供するため、修学旅行や課外学習の一環として、企業訪問プログラムを実施しております。
    • 内容:会社紹介、オフィス見学・社員との交流、「アプリの企画」等のワークショップ 略
  • その他受賞歴
    • 日本e-Learning大賞2016年最優秀賞受賞
    • GooglePlay ベストアプリ受賞(2015/2016 年)
    • Appstore Essentials 選出

補足

受賞者プレゼンテーション

speakerdeck.com

まとめ

まつもとさんが最後の審査委員長講評で「賞は時の運もある」と仰っていましたが、まさにその通りだと感じています。
今回、大賞を受賞できたのはStudyplusというサービスの実績や可能性、Rubyとの関わり方などを総合的に評価していただいた結果であり、タイミングも良かったのだと思っています。

この結果をひとつの糧にして、今後とも教育領域とRubyコミュニティへ貢献をしていく所存です。

透明Activityの罠

初めまして、Studyplus開発部でAndroidアプリの開発をしております中島です。

今回のブログでは、我々 Studyplus Androidチームが踏んでしまった、深い深い落とし穴について少しお話しさせていただければと思います。

透明Activityとは?

ここで 透明Activity と言っているのは、以下の指定を持ったThemeを用いることで背景を透過させたActivityのことです。

(Android SDK内、theme.xmlより抜粋)

<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>

弊社アプリでは、例えば以下のような箇所で使用しています。

  • 初めて使う機能の際に、チュートリアル画像を出して簡単に説明する
  • ホーム画面を開いた際、ユーザーが設定したイベント(模試などが多いでしょうか)までの日にちをカウントダウン表示する

透明Activity の罠とは?

では、この透明Activityにどんな罠があったかということについて、順を追って説明いたします。

Overture<発端>

弊社アプリは、今年の10/10のアップデートをもってTargetSDKを 26 から 27 に更新いたしました。 これ自体は特に問題ありません、むしろ素晴らしいことであると思います。(Androidチームとしては28まで上げたかったのは確かですが、まぁ一歩ずつ)

手元のテストでも特に問題はなくリリースしたのですが、その日の夕方から悪夢が始まりました…

Stampede<殺到>

Fatal Exception: java.lang.RuntimeException: Unable to start activity ComponentInfo{~~Activity}: java.lang.IllegalStateException:
Only fullscreen opaque activities can request orientation

複数のActivityで上記のクラッシュレポートが殺到しました。一晩で 10k を超えるクラッシュが報告され、はっきり言って異常事態でした。

Investigate<調査>

正直この時点で精神を落ち着かせることなど不可能ですが、頭だけでも落ち着かせて調査を進めなくてはいけません… Crashlyticsの確認と、ソースコードの確認で以下のようなことがわかりました。

  • クラッシュは Android 8.0 のみで起こっていること
  • クラッシュしているActivityは全て 透明Activity であること

また、エラーメッセージで検索したところ StackOverFlowの投稿 が見つかりました。 これに加えAOSPのActivity.java内部を8.08.1で比較してみましたところ、8.0で以下の処理が追加され、8.1で削除されていることがわかりました。

Activity.java(8.0)

protected void onCreate(@Nullable Bundle savedInstanceState) {

    ~~~

    if (getApplicationInfo().targetSdkVersion > O && mActivityInfo.isFixedOrientation()) {
        final TypedArray ta = obtainStyledAttributes(com.android.internal.R.styleable.Window);
        final boolean isTranslucentOrFloating = ActivityInfo.isTranslucentOrFloating(ta);
        ta.recycle();

        if (isTranslucentOrFloating) {
            throw new IllegalStateException(
                    "Only fullscreen opaque activities can request orientation");
        }
    }

    ~~~

以下は、この変更がAOSPに追加された時(66fa94d42f30771c3e7b249756aed656d38aed08)のコミットメッセージです。

This changelist enforces that activities targeting O and beyond can only specify an orientation if they are fullscreen. The change ignores the orientation on the server side and throws an exception when the client has an orientation set in onCreate or invokes Activity#setRequestedOrientation.

O以上において、透明Activity などのfullscreenではないActivityにはOrientationの指定を行えないようにすることが目的として明言されています。 こうすることで、直前のActivityのOrientationに依存させることが目的だと思われます。

また、その後に Oには指定を許す といった旨のコミットメッセージと共に コミット(d1ac18c7c9eca1b07120be598dc6859b188baeb3)が追加され、上記のコードの形になったようです。

Allow for SDK 26 Activities to specify orientation when not fullscreen.

Conclusion<結果>

調査の結果、以下の条件を全て満たした場合に、 Activity#onCreateで必ず例外をスローしてクラッシュする ようになっていることが発見されました。

  • アプリの指定する TargetSDKが 27(Android 8.1) 以上
  • 動作端末 が Android 8.0 (SDK 26)
  • ActivityのThemeに <item name="android:windowIsTranslucent">true</item> または <item name="android:windowIsFloating">true</item> と指定している
  • Activityに android:screenOrientationportraitlandscape を指定している

なお弊社アプリは縦画面固定アプリのため、アプリ内の全Activityに対し android:screenOrientation="portrait" が指定がされています。 そのため、TargetSDKを27に上げたことが、罠の最後のトリガーだったのでした…

Fix<対応>

対応としては、Orientationの指定を消せば解決する問題ではあります。 しかし、メッセージでは8.0 以上に限定しているため、ただOrientation指定を消すのみでは 8.0 未満の端末に影響する可能性があります。 以上の理由から、該当Activityにおいて以下のような修正を行ないました。

  • AndroidManifestからOrientation指定を削除
  • 8.0 未満の場合のみ、 Activity#onCreate でOrientation指定を改めて追加
super.onCreate(savedInstanceState);

// super#onCreate の後に改めて Orientation 指定
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}

この対応により事態は収束しましたが、次に私を待っていたのは障害報告書の作成任務であったことは想像にかたくありません。

まとめ

今回は、TargetSDKを上げたことにより顕在化した不具合についてお話しさせていただきました。

ただ、今回の一件も、元はと言えばTargetSDKを最新に近づけようという意志の下に生まれたものと言えます。 StudyplusにはFail Forward(失敗から学ぶことに重点を置き、失敗そのものを批判することはしない)という標語があります。 我々Studyplus Androidチームはこの一件から学んだことを糧にして、今後もモダンなAndroid開発を目指していければと思っております。(Kotlin Coroutinesの導入も始まっています!) もちろん同じ轍は踏まぬよう、十二分に注意いたします。

Studyplusでの開発に興味がおありの方はぜひ ご応募ください

AAC Pagingライブラリ導入の話

お久しぶりです。Androidエンジニアの若宮(id:D_R_1009)です。 今日はAndroid Architecture Components Pagingライブラリ(AAC Paging)の導入例を紹介したいと思います。

アプリを利用してくださっている方はお気づきになられていると思いますが、11月19日よりアプリのデザインを一新しました。 デザインの変更に伴い、アプリ起動時の通信処理や各Fragmentの生成処理の見直しを行いました。安定性が増すことを目標に、見直しに取り組みました。

デザインのアップデートとほぼ同時ではあるのですが、タイムラインのパフォーマンス改善にも取り組みました。 今回は、パフォーマンス改善でAAC Pagingライブラリを導入した際のアレコレを書きたいと思います。

AAC Pagingライブラリとは

https://developer.android.com/topic/libraries/architecture/paging/

AACライブラリの一つとして2018年6月に正式リリースされた、ローカルDBやリモートサーバーから逐次データを読み込む処理をサポートしてくれるライブラリです。 データ読み込みの処理と表示するデータの保持を分けることで、RecyclerViewのスクロール処理がデータの読み込み処理によって遅くならない等の効果が期待できます。

個人的には recyclerview.extensions.ListAdapter がsupportライブラリに入った頃から気になっている存在です。 そのため「ようやく正式版になってくれたか」という気分ですね。

導入に当たって検討した事柄

旧タイムライン処理にはいくつかの課題がありました。一部を抜粋すると下記の通りです。

  • 追加読み込み処理が「最後から5番目」を表示したタイミングになるため、読み込み処理がUI処理に強く依存している
  • リストのデータがRecyclerView.Adapterで保持されているため、Fragmentの破棄時にキャッシュが削除されてしまう
  • ObservableListを利用しているため、通信取得処理のスレッドとUI更新時のスレッドをFragment内で明示的に切り替える必要がある

Pagingライブラリの導入によって次のように解決すると考えました。

  • 追加読み込みのタイミングがPagingライブラリ側でハンドリングされるため、読み込みの通信処理に専念できる
  • リストのデータをメモリやディスク上で保持できるため、Fragmentの破棄時に(不要な)キャッシュ削除が発生しない
  • LiveDataでリストの取得ができるため、UIスレッドとそのほかのスレッドをほぼ意識しないで済む(ViewModel + LiveDataの処理にまとめられる)

導入方法

前述の通り、Pagingライブラリを導入していた時期はアプリのデザインリニューアルの時期とかぶっています。 また導入事例や導入LTなどをあまり見つけることもできなかったため、いくつか安全策をとることとしました

旧タイムライン処理との平行運用

通信時のパースクラスから表示用のFragmentまで、旧タイムラインの処理と切り離して全て新規作成することとしました。 他の新規開発機能とのバッティングを抑え、ライブラリの導入に失敗した場合や想定以上の工数を使ってしまった場合に備えるためです。 結果としてタイムラインを置き換えることはできましたが、タイムラインの一部のみ置き換えることを想定しました。

良かった点

  • 旧タイムラインを置き換え可能な箇所から書き換えることができたため、レビューのコストが想定より低くなった
  • 一部のみの置き換えを念頭に置いて設計した結果、依存度の低いコード記述となった
  • 旧タイムラインは主にJavaで記述されていたが、新タイムラインはKotlinで記述することができ、data classを活用できた

悪かった点

  • JSONのパースクラスから作り直したところ、RecyclerViewのItemレイアウトが使い回せなくなってしまった
  • 旧タイムラインに追加された修正を新タイムラインに反映させていたが、反映漏れが発生した

その他に新しくなった箇所

旧タイムラインのItemをConstlaintLayoutに置き換えたところ、View間の関係性が漏れていた箇所が多数ありました。 Viewの縦横のサイズ計算に関わる問題となるため、ネストが浅くなるようリファクタリングを行なっています。

また、タイムラインのFragment間でRecyclerViewのViewPoolを共有し、Inflate回数を減らす対応を行いました。 こちらは2018年のDroidKaigiより広く使われている手法なので、弊社アプリもようやく追いついた感覚があります。

感想

導入のためサンプルコードをPCに落としてから、機能としてリリースするまで3週間ほどかかりました。 ほとんどPagingライブラリの導入にかかりきりだったので、長い時間をかけたような気がしています。

導入時に想定していたほど、Pagingライブラリの導入にはコストがかかりませんでした。 これは公式で非常にわかりやすいサンプルプロジェクトが用意されていたことが大きいです。まず公式サンプルに従ってAPIとDBの接続をするのが良さそうです。

逆に想定しきれていなかったのが、レイアウト周りの修正の多さです。 もともとButterKnifeをDataBindingに置き換えている途中だったこともあり、Kotlinとxmlの両方で書き換え処理が多くなりました。 変更に強いデータ構造を作らなければならない、という強い反省になったと感じています。

ひとこと

バリバリAACを使っていきたい! という気持ち溢れるエンジニアがいましたらこちらよりご連絡ください。

Kubernetesハンズオンを行いました

はじめまして、Studyplus開発部でサーバーサイドを担当している栗山です。

背景

Studyplusではコンテナ化を進めたいと思っていますが、まだAWS Elastic BeanstalkEC2上でアプリケーションが動いています。 今後コンテナ化するにあたり、

  • コンテナ化を進めていきたいが、コンテナのデプロイや管理はどうすればいいかわからない。
  • コンテナ管理するためにKubernetesというのが良いらしいが覚えることが多くて難しそう。
  • そもそもコンテナよくわからないし、敷居が高そう

といった不安や疑問が部内にあったのでそれらを解消すべく、Kubernetesのハンズオンを実施しました。

目的

ハンズオンの目的としては以下です。

  • Kubernetesが何を解決するかを知る
  • Kubernetesの機能を知る
  • Kubernetesの各概念を知る
  • GCP上のKubernetesに簡単なアプリケーションをデプロイして、実際にKubernetesのスケーリング機能や耐障害性等々を体験してもらう。

ハンズオンの進め方

ハンズオンの資料は以下にあります。

まず最初にKubernetesの特徴や何ができるか、そして各概念の説明をしました。

  • Kubernetesの各概念の理解 (※説明が間違っている可能性があるので間違いを見つけた場合はPRを送ってもらえると助かりますm( )m)

Kubernetesには様々な機能、リソースがありますが、まずは基本であるPod, ReplicaSet, Deployment, Service, Ingressを理解してもらうことに重点をおきました。

その次に、実際にGCP上でアプリケーションを動かしてもらいました。

GCPを選んだのは

  • GCPは2014年からKubernetesをマネージドサービスとして提供しており、Kubernetesとの親和性が高い
  • Studyplusでは主にAWSを使っているので、違ったクラウドを体験することは見識を広める意味でもプラスになる

といった理由からです。 実際触ってみると、GCPは非常にシンプルでKubernetesがシームレスに組み込まれており、Kubernetesを動かすには最適な環境だと感じました。

このハンズオンは、説明と実際に手を動かすのに全部で1時間半〜2時間くらい必要となります。 そのためスムーズに進める必要があり、以下のことを注意して行いました。

  • ハンズオン資料のコードをコピペすれば動くようにしておく
    • しかしコピペを間違う人もでてくるので、必要なファイルは全て予め用意しておきダウンロードすれば済むようにした
  • 事前に時間がかかる作業(例えばGCPアカウント作成や、SDKインストール作業等々)はハンズオンの前に各自で行ってもらう
  • 出来るだけ早く理解してもらうために、説明文はできるだけ丁寧かつ正確に書いておく。また喋る予定の内容も全て書いておいて読めば分かるようにしておく。
  • 予期せぬエラーがでたり躓いて先に進めない人が出ないように、問題なく進めているかこまめに確認を取る。人数が多いと躓く人が多くなるので、一回のハンズオンの参加者は4人くらいにした。

まとめ

Kubernetes自体の素晴らしさもあり、ハンズオン後はKubernetesを使っていく機運が高まりました。 実際に運用していないのでまだ見えていないつらみやデメリット等があると思いますが、それを上回るメリットがあるように感じているので、今後はKubernetesの導入を進めていけたらと思っています。

スタディプラスではKubernetes導入を一緒に進めていく仲間を募集しております! ご応募お待ちしております!!

補足

ハンズオンの内容は、 WEB+DB PRESS Vol.99のKubernetes特集の内容を参考にしました。ありがとうございました。 また、Kubernetesを知るためにはKubernetes完全ガイドという本が2018/9に発売されており、これを読めば完全に理解できると思います。本当に最高の本です。

インフラエンジニアがオフィス移転で考えたこと[前編]

初めまして、インフラエンジニアの菅原です。

弊社では事業拡大に伴うオフィス移転を予定しております。
インフラエンジニアとして社内ネットワーク移設について検討したことを移転前と移転後の2回に分けてまとめたいと思います。

f:id:ksugahara08:20181110143139j:plain:w300

まだ何も物が入っていないまっさらなオフィスの写真です。結構広い!
移転は年末になり、現在は移転準備をしている最中となります。

移転前に検討するべき項目

1.移転後の要件

まず、移転後の社内ネットワークデザインを決める基礎となる情報を収集しました。

  • 社内ネットワークの利用ユーザー数
    弊社では事業拡大も視野に入れて50人~200人規模の利用ユーザー数を想定しました。
    それだけでなく、イベント時はゲストも繋げるため最大200人の同時接続を許容する必要がありました。

  • ネットワーク接続する端末の台数
    アプリの検証端末などもあるため利用ユーザー数+αの接続が見込まれます。接続数には余裕を持たせたほうが良さそうでした。

  • オフィスのレイアウト
    今回は内装業者さんに座席の位置やパーティション等の図面を早い段階で頂いたため、そちらを基にルーターの設置場所、無線アクセスポイントの配置について検討することにしました。

  • オフィスで行う業務
    一般的な事務、カスタマーサポート、WEBアプリ開発がオフィス業務の中心になります。
    AWSとGCPを利用しているため、サーバーの移設については今回検討不要でした。
    オフィス向けプリンター・複合機は現在利用しているものを利用します。

2.現状の確認

現状どのような業者と契約しているか、どのような機器を利用しているかを把握し、移転後も使い続けるのかを確認する必要があります。今回は以下の現状について確認しました。

  • ISP(インターネットサービスプロバイダ)と光回線業者
    過去の経緯からIP電話とインターネット回線を別々の業者と契約している状況でした。こちらを一つにまとめられないか検討しました。しかし、変更してもランニングコストは変わらなかったため、現状のプランのまま移転することにしました。

  • ネットワーク機器とその設定
    ルーター、無線アクセスポイントについて設定内容を確認しました。現状では接続先アクセスポイントによって回線速度が遅いと感じていました。こちらも移転に伴って変更を検討しました。

  • 現状の回線速度
    引越し後の回線速度とも比較を行いたいため、測定しておきます。ツールは「Speedtest by Ookla」を 利用しました。

  • 固定IPが使われている箇所
    社内ツールなどは固定IPでアクセス制限を行なっている箇所があったため、設定を洗い出しました。
    移転に伴って設定を書き換える必要があります。

3.障害・災害時の検討

SLOによって変わってくるとは思いますが、機器の故障に寄る障害にも強い構成を検討するべきでしょう。
今回の移転ではSPOF(単一障害点)になりうる部分をできるだけ排除し、ネットワークの可用性を向上させることにしました。
具体的には現状ルーターが1つしかないので、機器の故障に備えてもう一つ購入し、多重化することにしました。 業者の保険に入ることで故障時の対策とすることもできましたが、ルーターを2つ購入して設置しておくことで、素早く復旧できると考えたからです。

4.スケジュールの策定

ネットワークの動作確認を考えると、移転の1週間前までには移転先でのネットワーク設定作業を終えていたいと考えました。オフィス移転の前日や前々日がネットワーク移転の作業日になるケースは多いと思います。そうするとスケジュールに余裕がなく、もしもの時に対応できません。
こうした要件を満たしたい場合に問題となってくるのが光回線業者の作業です。 「移転」で依頼した場合は工事作業日が前日や前々日になってしまします。しかし、「新規」で契約し直すことで現オフィスと平行契約期間を設けることができます。これで1週間前に動作確認ができるようにしました。

5.移設後のネットワークの構成と機器を検討

  • ルーター
    現状使っているルーターが故障した時のために同じものをもう1台購入することにしました。
    移転後のオフィスでは利用者が増えるためルータのNAPTテーブルエントリ数(IPマスカレードエントリ数)は購入前に確認しました。 PC1台に対して50〜200程度あれば十分だと思います。
    スループットも重要な確認ポイントになります。 こちらが低いと回線速度を遅く感じられてしまいます。 大雑把ですが、実測値で1Gbps以上出るものを選ぶと良いと思われます。

  • 無線アクセスポイント
    移転前のオフィスでは無線アクセスポイントが2つしかなく、接続先によっては遅く感じられました。
    そのため、移転先のオフィスでは無線アクセスポイントを5つに増やすことにしました。 天井に設置し、電波の強い範囲が被るようにする予定です。
    これでインターネット回線が遅いと言われないようになって欲しいです(願望)。

まとめ

今回はオフィス移転前に社内ネットワークについて検討したことを中心にまとめました。
社内要件を行い、業者さんとコミュニケーションを取りながら移転に向けて準備を進めております。
次回は移転後の話を記事にしたいと思います。

弊社では新しいオフィスで一緒に働いてくれる仲間を募集しております!
ご応募お待ちしております!!