GAE/Goで位置情報マッチングAPIの設計を考える
先日、サイバーエージェントさんの学生版ガレスタというインターンに参加しました。
6週間で企画からサービス完成までを4人(ビジネス1人, デザイナー1人, エンジニア2人)で行うというものでした。
今記事ではこのインターンで得た技術的なことを、つらつら書いていきたいと思います(´・ω・`)
要件
位置情報を用いたマッチングモバイルアプリのAPI
技術選定
言語
- チャット実装時のマルチスレッド処理が簡潔
- 薄くAPI作れるから短期間開発○
- 丁度いい型安全
- 開発効率を上げるエコシステムが整ってる
- モバイルAPIの実例多い
- インターン後仲間集める時に、若く優秀なエンジニアを集めやすい
http通信
- 薄い
- 慣れてる
- 標準なのでバージョン管理安心
ORマッパー
RDB
- 慣れてる
KVS
- 3.2から追加された"Geo"系コマンドを使って位置情報管理したい
インフラ
- サーバーサイドは一人なのでとにかく開発に集中したい
- PaaSの中で最も妥当な柔軟性、料金と感じた
アプリケーション設計
$GOPATH ├── app │ ├── app.yaml │ ├── config.go │ ├── handler.go # Application層 │ └── main.go # UI層 └── src ├── domain # Domain層 │ ├── config.go │ ├── detail │ ├── feed │ ├── location │ └── registration ├── infra # Infrastructure層 │ ├── cache │ ├── config │ ├── objstorage │ └── orm └── middleware
レイヤードアーキテクチャを用いた軽量DDDを採用しました。
DDDを完璧に実現しようとすると、分析、設計、実装というフローを踏まないといけないのですが、今回は短時間且つ仕様も変わっていくため、分析はせず軽量なDDDを実現しました。
DDDについてはまとまった知識がなかったので、今回エリック・エヴァンスのDDD本を初めて読みました。
GAEは、app.yamlが置いてあるディレクトリ以下を全てビルドしてしまうので、Domain層以下は別packageとして管理しています。
集約毎にディレクトリを分け、その中でentity、value object、repositoryを定義しています。
infra配下ではDBに依存するコードをまとめ、出入りするデータをrepositoryにinterfaceとして定義しておきます。
このようにすることで、ミドルウェアの差し替えを容易にしています。
GAE/GoのDDD設計はまだまだデファクトスタンダードが定まっていないようなので、これからも熟考していこうと思います。
インフラ設計
全体アーキテクチャ
ほとんどのデータはCloudSQLにMySQLを乗せて管理しました。
CloudSQLはフェイルオーバー時に数分のダウンタイムが発生してしまうというデメリットがありますが、短期開発ということで立ち上げ時の設定が楽であるという最大のメリットを汲み採用しました。
CloudStorageでは画像中心に、サイズの大きいオブジェクトを管理しています。
特筆すべきはredisです。
redisによる位置情報管理
位置情報機能の要件は"近い順にアクティブユーザーを返す"でした。
そこで最初に考えたアーキテクチャはこんなものでした
列志向のBigtableに位置情報を溜めていき、アクティブユーザーを返す度にBigQueryを回すというものです。
ありえませんね。
ユーザー毎にクエリを回すというこのモデルではパフォーマンスにもお財布にも優しくないですね。
サイバーエージェントの社員さんに聞いたところ、redis3.2から位置情報を扱うコマンドが追加されたらしく、使ってみることにしました。
>>Aさんから半径100km以内のユーザーを近い順に頂戴!<<
わがままですね。
でもこんなわがままを、ジェントルマンなredisは1コマンドで叶えてくれます。
GEORADIUSBYMEMBERという夢のコマンドをredisにささやきます。
// GEOADDで追加 > GEOADD location 13.583333 37.316667 "Aさん" 13.361389 38.115556 "Bさん" (integer) 2 // GEORADIUSBYMEMBERで取得 > GEORADIUSBYMEMBER location "Aさん" 100 km 1) "Aさん" 2) "Bさん"
GEOADDでsorted set型の集合にぶっこみ、ソートします。
GEORADIUSBYMEMBERを使用すればO(N+log(M))で検索することが可能です。
*N=指定範囲内の要素数, M=インデックス内の項目数
これで当初よりシンプル&ハイパフォーマンス&低料金で要件を実現することが出来ました。
社員さんありがとうございます〜〜
テーブル設計
ゼロイチでDB設計したことなかったので、しっかり以下3つ読みました。
プライマリーキーをとりあえず"id"というサロゲートキーにしてしまうアンチパターン「idリクアイアド」を避け、
なるべく自然キーを用いた設計を心がけました。
API設計
ゼロイチでAPI設計したことなかったので、しっかりこれを読みました。
リソースのページネーションを実現する際、最初は相対位置を利用していましたが、絶対位置を利用することでパフォーマンスと整合性の改善をしました。
まとめ
何が言いたいのかまとまっていませんが、とりあえず多くの技術的学びがあったということです。
痛感したのは、どんなに短期開発でも時間かけるべき部分には大いに時間をかけるべきだということです。
どこを丁寧に行うか適切に判断することが出来れば、スピードと品質は必ずしもトレードオフにならないと感じました。
このアプリは近いうちにリリースするので、その時にまた報告致します。
Goで軽量なスクレイピングライブラリを作ってみた
リポジトリ
はじめに
GoでWebクローラーを開発する際、皆さんどうされてますか??
フルスクラッチで作るのは少し面倒だし、asciimoo/collyのようなフルスタックなのはいらない、、という時に丁度いいライブラリがなかったので作りました!
本当に最低限の機能のみ搭載しました。
機能
主な3つの機能を紹介します。
オーガニック検索
キーワードを入れて自然検索結果画面に表示されるページURLを返します。
import "github.com/ryonakao/netsurfer" urls, err := netsurfer.OrganicSearch("キーワード", 1)
順位調査
指定キーワードで検索した時、指定ページがオーガニックで何位に掲載されるかを返します。
import ( "net/url" "github.com/ryonakao/netsurfer" ) u, _ := url.Parse("https://qiita.com/ryonakao") rank, _ := netsurfer.GetRank(u, "ryonakao", 2)
HTML取得
指定ページの静的ページを返します。
import "github.com/ryonakao/netsurfer" html, err := netsurfer.GetHTML("https://qiita.com/ryonakao")
実装一部紹介
SERPsのurlスライスを返す以下の関数を御覧ください。
ユーザーにdepthを指定してもらうことで、一回のスクレイピングで全SERPsを取得しています。
*ryonakao/netsurderはPuerkitoBio/goqueryに依存しています。
まとめ
ライブラリ開発めちゃんこ楽しい♪
どんどん作っていこうと思います!!
GoのcliツールUIライブラリを試す ~ 今日のGithub Trending ~
今日のGithub Trending
カリスマエンジニアになりたい!!
そう願う大学二年生の私は新たな企画を始めます\(^o^)/
題して、 >>今日のGithub Trending<<
Github Trendingを眺めて、面白そうなリポジトリがあったらガンガン試していきます。
Github Trendingとは、盛り上がってるリポジトリを探すことが出来るというGithubが提供するサービスです。
第一回のリポジトリは、今日だけで200スター以上ついているmarcusolsson/tui-goを試していこうと思います!!
チャットクライアント風なcliツールを作ってみる
完成図
実装
基本的に以下の2つの構造体を使って作っていきます。
box.go
ボックスはウィジェットを水平または垂直に配置するためのレイアウトです。 もし 水平にすると、すべてのウィジェットは同じ高さになります。 縦にすると、すべて同じ幅になります。
// Box is a layout for placing widgets either horizontally or vertically. If // horizontally, all widgets will have the same height. If vertically, they // will all have the same width. type Box struct { WidgetBase children []Widget border bool title string alignment Alignment }
entry.go
エントリは、1行のテキストエディタです。 これによってユーザはアプリケーションにテキストを供給することができます。例えば、ユーザおよびパスワード情報を入力することができます。
// Entry is a one-line text editor. It lets the user supply the application // with text, e.g., to input user and password information. type Entry struct { WidgetBase text string onTextChange func(*Entry) onSubmit func(*Entry) }
サイドバー作成
sidebar := tui.NewVBox( tui.NewLabel(" <CHANNELS>"), tui.NewLabel(" general"), tui.NewLabel(" random"), tui.NewLabel(""), tui.NewLabel(" <DIRECT MESSAGES>"), tui.NewLabel(" maria"), tui.NewLabel(" john"), tui.NewSpacer(), ) sidebar.SetBorder(true)
- NewVBox関数で、縦方向のBoxを作成できます。
- 任意数のWidgetを与えることが出来ます。
- SetBorderメソッドで枠を表示するかを決めます。
送信済みメッセージを表示
type post struct { username string message string time string } // 送信済みPostのスライス var posts = []post{ {username: "ryo", message: "Hello!", time: "14:41"}, } history := tui.NewVBox() history.SetBorder(true) history.Append(tui.NewSpacer()) for _, m := range posts { history.Append(tui.NewHBox( tui.NewLabel(m.time), tui.NewPadder(1, 0, tui.NewLabel(fmt.Sprintf("<%s>", m.username))), tui.NewLabel(m.message), tui.NewSpacer(), )) }
NewVBox関数で縦方向のBoxを作成し、Appendメソッドでメッセージを追加していきます。
Appendメソッドの実装を覗いてみましょう。
box.go
// Append adds the given widget at the end of the Box. func (b *Box) Append(w Widget) { b.children = append(b.children, w) }
Appendメソッドは、Widgetインターフェイスの値をBoxのchildrenというスライスに追加していきます。
入力ボックス作成
input := tui.NewEntry() input.SetFocused(true) input.SetSizePolicy(tui.Expanding, tui.Maximum) inputBox := tui.NewHBox(input) inputBox.SetBorder(true) inputBox.SetSizePolicy(tui.Expanding, tui.Maximum)
Entry構造体を作ってNewHBoxの引数に渡すことで、Entry構造体を子要素として持ったBoxを作ることが出来ます。
NewHBoxメソッドの実装を覗いてみましょう。
box.go
// NewHBox returns a new horizontally aligned Box. func NewHBox(c ...Widget) *Box { return &Box{ children: c, alignment: Horizontal, } }
NewHBox関数はWidgetインターフェイスを子要素に含めることが出来ます。
そのためBox構造体だけではなく、Widgetインターフェイスが実装されているEntry構造体も渡すことができます。
投稿する
import "flag" var name = flag.String("u", "kazuki", "Set your name") flag.Parse() input.OnSubmit(func(e *tui.Entry) { history.Append(tui.NewHBox( tui.NewLabel(time.Now().Format("15:04")), tui.NewPadder(1, 0, tui.NewLabel(fmt.Sprintf("<%s>", *name))), tui.NewLabel(e.Text()), tui.NewSpacer(), )) input.SetText("") })
Entry構造体のOnSubmitメソッドで、Enterを押した時の挙動を設定出来ます。
historyにメッセージを追加します。
flagパッケージを使用して、実行時にユーザー名を指定出来るようにします。
UI作成
chat := tui.NewVBox(history, inputBox) chat.SetSizePolicy(tui.Expanding, tui.Expanding) root := tui.NewHBox(sidebar, chat) ui := tui.New(root) ui.SetKeybinding("Esc", func() { ui.Quit() })
- Boxの、Widgetインターフェイスを子要素に出来る機能を利用して、今まで作成したBoxをrootにまとめていきます。
- tcellUI構造体を作成し、Esc押下時にuiを終了するよう、キーバインディングを設定します。
実行
if err := ui.Run(); err != nil { panic(err) }
Runメソッドを覗いてみましょう。
ui_tcell.go
func (ui *tcellUI) Run() error { if err := ui.screen.Init(); err != nil { return err } if w := ui.kbFocus.chain.FocusDefault(); w != nil { w.SetFocused(true) ui.kbFocus.focusedWidget = w } ui.screen.SetStyle(tcell.StyleDefault) ui.screen.EnableMouse() ui.screen.Clear() go func() { for { switch ev := ui.screen.PollEvent().(type) { case *tcell.EventKey: ui.handleKeyEvent(ev) case *tcell.EventMouse: ui.handleMouseEvent(ev) case *tcell.EventResize: ui.handleResizeEvent(ev) } } }() for { select { case <-ui.quit: return nil case ev := <-ui.eventQueue: ui.handleEvent(ev) } } }
- 別ゴルーチンを立ててPollEventメソッドを呼ぶことで、イベント発生を監視します。
- イベントを検知したら、eventQueueというチャネルにenqueueしていきます。
- eventQueueの状態をメインゴルーチンで監視し、受信を確認したら実行します。
*PollEventはgdamore/tcellという別パッケージのメソッドです。
コード全貌
長くなるのでGIstに記載しました。
https://gist.github.com/ryonakao/6247637a39f32ba0543f5b0421297357
まとめ
評価されてるライブラリを試すことで以下のメリットがあることに気づきました。
- コードを多読することになるため、コーディングスキルがUP(今回は、marcusolssonさんのinterface設計がとても勉強になりました。)
- OSS界隈での需要が分かる
- ワクワクする
また時間があったらやってみたいと思います!!
オレ流学習フレームワーク
はじめに
今記事では、大学生の私が独学でコンピュータサイエンスを学習する際に使用している独自の学習フレームワークを紹介していきます。
あくまで持論なので、すべての人に最適であるとは限りません。
これから紹介するのは、いかに楽して知識を得られるかを念頭に置いたフレームワークです。
ここで言う"楽"というのは、無駄な体力を使わないという意味です。
他にこんな学習の仕方してるよ〜というのがあれば、コメントして頂けると幸いです!
主な学習フレームワーク
発信ドリブン型学習
ブログ等で発信したり、人に教えることを前提としたインプットを行います。
インプットしながらどうアウトプットするかを同時に考えることで、高速な理解が可能になります。
なぜ高速な理解が可能になるのでしょうか?
理解とは、脳の棚に整理された形で分けられている状態だと私は考えています。
人に伝えるためには、内容を整理しないといけません。
インプット中に棚に振り分けていくことで、インプット終了後には既に"理解した"という状態になっているという考え方です。
これを習得すると、書籍を読んでいる時に文章が構造的に見えてくるようになります。
遅延評価型学習
これは有名な遅延評価勉強法のことで、その知識が必要になった時に初めて勉強する方法です。
遅延評価とは、元々はプログラミング手法の一つで、必要になるまで値の評価をしないという意味の言葉です。
"必要は発明の母"という言葉がある通り必要性を感じた時の吸収力はかなり高いため、必要度に比例して吸収力も上昇します。
「直近で必要ではないけど、今学習しておきたいことだってあるぞ!」
そう思う方も少なくないと思います。
実際私も、ネットワークの仕組みやミドルウェアの特性等、学生生活で必要になることがほとんどない学習をしています。
別に必要になるのを待つ必要は無いのです。
必要を作り出せばいいのです。
先日私は、ネットワークの仕組みやミドルウェアの特性を理解しないと勝てない大会に出場することを決め、必要を作り出しました。
その時のレポート↓
CyberAgentのアドテクチャレンジで正確なDMPを作る - 爆速でGo!
このように、遅延評価型学習では必要をコントロールしていくことがキーとなってくると考えています。
フレームワークをどう組み合わせるか
爆発的な吸収力を生む遅延評価型でインプットするところから始めます。
しかし遅延評価型でバラバラに集めた情報はTipsにはなるのですが、知識にはならないと思います。
知識にするためには、ある程度網羅的なインプットが必要です。
そこで発信ドリブン型を使います。
網羅的なインプットというと、分厚い書籍を1ページ目から最後まで順に読むという印象がありますが、これは中々体力を使いますね💦
これでは冒頭で述べた"楽して"という言葉に反します。
ここで私は、独自の読書フレームワークであるgoto文型リーディングを使います。
goto文型リーディング
goto文とはプログラミングの制御構造の一つで、指定された場所に処理をジャンプさせるための文です。
このフレームワークは、各章が独立しておらず順に読まないと意味が分からないタイプの書籍に有効です。
手順は以下の通りです。
目次を見て、この書籍ではどんな内容に触れているかにしっかり目を通す。これは毎回読む前に行う
一番興味ある部分から読む
興味が無くなるまで以下を繰り返す
意味が分からないワードや表現が出てくる
目次が頭に入っていれば、その書籍のどこかで説明されていることが分かる
該当箇所にジャンプする
書籍によりますが、これで大半は読むことが出来ます。
大半を読めばその書籍の内容に関してはかなり理解度があるため、残りの部分は小見出しに魅力がなくても興味が湧くことが多いです。
それでも興味がない部分は無理に読む必要ないと思います。
そっと本棚にしまいましょう。
モチベーション・マネジメント
上記を読んで正直面倒くさいと思った方もいると思います。
私も同感で、上記を完遂するためには一定のモチベーションが必要です。
上記を行うためにはモチベーション・マネジメントを常に行う必要があります。
私は、学習をしていく上でこれが一番重要だと思います。
夢や目標が遠すぎると挫折しがちですよね、、
私は、ちょっと頑張れば届く中期目標向かって進んでいる時が一番モチベーションを高く保つことが出来ます。
この中期目標の距離がどのくらい近いと良いのかは人によって違うと思います。
中期目標を最適な場所に置き続けることが大事だと思います。
これはスキルだと思っています。
環境が変化し続ける中で、常に適切な場所に中期目標を置けるスキルがある人の成長は止まらないと思います。
まとめ
最後まで読んで頂きありがとうございます。
冒頭でも申し上げましたが、あくまで自分の中でしっくりきているフレームワークなので、理解し難い部分があると思います。
学習の質をもっとあげていきたいと思っているので、他に実践されているものがあれば、教えて頂けますと幸いです!
CyberAgentのアドテクチャレンジで正確なDMPを作る
先日株式会社サイバーエージェントの2dayインターン、アドテクチャレンジに参加させて頂きました。
そしてなんと優勝させて頂きました!
私はインフラ設計の経験がなく、常に手探り状態でした💦
今記事では、優勝に至るまでの技術的苦悩をまとめていきます。
問題
概要
開発時間終了後、位置情報やIDFAなどのデータが3時間程度大量に送られてくる(2000QPS)。
1リクエストにつき100ms以内に返さないと無効になる。
送られてきたデータを元に問題を解答し、その精度を元に得点がつけられる。
*ちなみにGoogle Cloud Platform使い放題\(^o^)/
詳細
以下の2つの形式でそれぞれ何問か答える
【バッチ問題】
データ送信が完了した後に問題に答える
ex)○時から○時の間に〇〇から半径10km以内にいた人数は?
【リアルタイム問題】
データ送信中に、特定IDFA(端末ごとのID的なやつ)と時間がjsonで送られてくる。
そのユーザーがその時間にどこにいたか等を答える。
1000ms以内に返さないと無効。
アーキテクチャ
最初の案
リアルタイム問題はレスポンスタイム制限があるため、直近で使うデータだけredisに置いておかないと💦
と思っていたのですが、1000msは余裕すぎました。
しかも直近で使うデータなんか分からないから却下。
二番目の案
1000msは余裕と分かり、今回ネックなのは書き込みであることに気付きました。
正直2000QPSが想像つかなかったのですが、直接書き込むのではなくジョブキューを噛ませるのが安全だと考えました。
GCPのCloud Launcherなら、redisがプリインストールされているインスタンスを素早く立ち上げられるし、
クラスタリングも比較的簡単に出来そうだったので決まりか!?と一瞬笑顔になりました。
しかしredisはインメモリDBなので、ログを1つでも取りこぼしてはいけない今回は不適切でした。
三番目の案
様々なミドルウェアを検討しましたが、フルマネージドサービスで統一した方ががつなぎ込みも楽だと考えたので、Cloud Pub/Subを採用しました。
データストアにはBigtableを採用しました。GCPサービスの中では最も時系列データを扱うのに向いていたからです。(後ほど説明します)
さらにバッチ問題用にBigQueryを採用しました。集計が速く、且つ全員が慣れていて学習コストが低いSQLで操作できるためです。
データをBigtableからBigQueryにエクスポートしようと思っていたのですが、そのためのBigtableのリージョンがus-centralとeurope-westしかありませんでした。。。
そこでログをリアルタイムでエクスポートしてくれるStackdriverLogging採用。(後ほど審査時に突っ込まれます)
完成!・・・
しかし全員が口をそろえて言いました。
時間足りなくね(-.-;)
採用案
とりあえずQPS2000でリクエストを流してみたところ、ジョブキュー噛ませなくても余裕で耐えられることが判明しました。
ということで、APIから直接Bigtableに書き込むアーキテクチャで決定しました。
言語
Go
理由
2日間という短い期間で高トラフィックを捌くには最適
- 実行速度○
- ビルド速度○
- go fmtのお陰で規約がいらない
- GCPサービスとの相性良さそう
- みんなのGoモチベが高い (超大事)
Bigtableのスキーマ設計
問題は時間指定が多かったため、時間順にソートしておきたいと考えていました。
リファレンスを読むとこんなことが書いてありました。
Cloud Bigtable ではデータが非構造化列として行に保存され、各行には行キーがあり、行キーは辞書順で並べ替えられています。
そこで行キーを’timestamp#IDFA'という形式にすることで、探索アルゴリズムの高速化を図りました。
ex)'2017-07-03 10:57:23 +0000 UTC#xxxxxxxxxxxxxx'
StackdriverLoggingとAPIのつなぎ込み
以下の数行コードで通常のstring型ログの書き込みが可能です。
Payloadというinterface{}型フィールドに文字列を渡します。
import "cloud.google.com/go/logging" var logClient *logging.Client logger := logClient.Logger("テストログ") logger.Log(logging.Entry{Payload: "テストテキスト"})
StackdriverLoggingはBigQueryで爆速集計するために採用したため、構造化したログを渡すことが必須でした。
BigQueryにはJsonPayloadという型があるため、Payloadフィールドにjsonで渡すのが最適解なのでは?という仮説のもとリファレンスを漁りましたが、のってませんでした(;´Д`)
GCPのソースコードを読んでみました
cloud.google.com/go/logging/logging.go 506~509行目
// Payload must be either a string or something that // marshals via the encoding/json package to a JSON object // (and not any other type of JSON value). Payload interface{}
jsonオブジェクト渡せってことかな??
早速以下のように構造体をエンコードして[]byte型に変換!
import ( "encoding/json" "cloud.google.com/go/logging" ) type Info struct { Latitude string `json:"latitude"` Longitude string `json:"longitude"` DeviceID string `json:"device_id"` SysName string `json:"sysname"` SysVer string `json:"sysver"` Timestamp string `json:"timestamp"` } info := &Info{} i, err := json.Marshal(info) logger := logClient.Logger(logName) logger.Log(logging.Entry{Payload: i})
エラー!!
深くまでソースを見てみると、
cloud.google.com/go/logging/logging.go 637行目
jb, err := json.Marshal(v)
Payloadフィールドの値を最終的にエンコードしてる!!
普通に構造体渡せば良かったのね!!!
import "cloud.google.com/go/logging" type Info struct { Latitude string `json:"latitude"` Longitude string `json:"longitude"` DeviceID string `json:"device_id"` SysName string `json:"sysname"` SysVer string `json:"sysver"` Timestamp string `json:"timestamp"` } info := &Info{} logger := logClient.Logger(logName) logger.Log(logging.Entry{Payload: info})
無事StackdriverLoggigにjsonログが流れました。
BIgQueryには1カラムに複数のデータを入れることが出来るRECORD型というデータ型があります。
jsonログをStackdriverLoggingに流すことで自動的にRECORD型のカラムを生成してくれます。(設定は必要)
オペレーション
開発時間が終わり、いよいよデータ送信の時間になりました。
すごい速度でリクエストが流れ、興奮していたのも束の間。
http: Accept error: accept tcp [::]:8080: accept4: too many open files
開始30分後、テスト時には発生しなかったエラーが!!
TCPが開いたままになってるのかな??と思いプログラムコードを確認したところ、リクエストボディを閉じ忘れていました。
func collect(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() // some codes... }
すぐにデプロイ!
Goはシングルバイナリを上げればいい!素晴らしい!
http: Accept error: accept tcp [::]:8080: accept4: too many open files
なぜだ、、、
KeepAliveオフにしたら解決するかな、、
net/http/request.go 203~206行目
// For client requests, setting this field prevents re-use of // TCP connections between requests to the same hosts, as if // Transport.DisableKeepAlives were set. Close bool
Closeという公開フィールドをfalseにすればDisableKeepAlives
にできるっぽい!
関数が用意されてない時点で非推奨だと思うけど、やるしかない!
func collect(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() // some codes... r.Close = false }
http: Accept error: accept tcp [::]:8080: accept4: too many open files
1分で死にました。
結局データ送信中は、人間が常にstatusを監視するという力技で乗り切りました笑
大会終了後に社員さんに聞いたところ、linuxのファイルディスクリプタ数(同時に開けるファイル数)の上限を上げれば解決したそうです。
何が起きていたかというと、
- ファイルopenしてはcloseを繰り返す
- リクエストが急増するとcloseがopenに追いつかず、too many open filesが発生
勉強になりました。
このような、知らないと解けない問題を解けるエンジニアになりたいです。
まとめ
とにかく死ぬほど楽しかったので、サーバーサイドに興味ある学生さんは絶対参加するべきだと思います!
技術的なことしか書きませんでしたが、チームビルディング等、多くのことを学びました。
また参加したいと思います!
*インフラ初体験の大学生2年生が書いてるので、間違いがあるかもしれません!
もし見つけたら指摘して頂けるとありがたいです。
優勝やったー!
2017年は「決める年」
今週のお題「2017年にやりたいこと」
タイトルに書いたように、2017年は「決める年」にします。
以下に記す中期的な目標のために具体的な3つの目標を定める。
- 人生哲学(人生で何をしたいかという最上級の目標。心理学的には究極的関心というらしい)を「決める」
- 全てにおいて「決める」スピードと精度を高める
- めりはりをつけるために逐一何時までになにをするかを「決める」