freeeのエンジニアとコロナ

この記事は freee Developers Advent Calendar の 24 日目の記事です。

こんにちは @hiraguri です。 freeeの開発担当取締役をやっていて、エンジニアチームの組織作りを主にやってます。CDOという役割ですが、DesignではなくDevelopmentです。わかりにくめです。

今日はfreeeのエンジニアチームがコロナ下のリモートワークをどう過ごしているかを共有しようと思います。 各社でさまざまな工夫をされてると思いますが、一事例として参考になれば嬉しいです。

リモワの始まり

freeeではコロナの急拡大をうけて、2月25日から全社リモートワークを開始しました。 いまも全社リモートは続いていて、ちょうど今日で丸10ヶ月になります。

健康状態の可視化

freeeでもリモートワークの環境に慣れず体調を崩してしまう人がでてきていましたが、直接顔をあわせないので体調を崩してしまうまで健康状態がわかりにくいことが悩みでした。 今年の7月からパルスサーベイという意識調査を定期的にやることで、健康状態の可視化を試みました。

設問は3つ

  • 仕事は充実してますか
  • 人間関係はどうですか
  • よく眠れてますか

回答は「全然良くない」から「とても良い」まで5段階とし、「良くない」の割合(低スコア率)をだしています。

f:id:hiraguri-hiraguri:20201223225626p:plain
パルスサーベイ(全社)

f:id:hiraguri-hiraguri:20201223225712p:plain
パルスサーベイ(エンジニア)

睡眠の低スコア率がきれいに下がっていて(眠れるようになった!)安心です。 9月25日の調査で10%あたりまで下がっているので、人間は大体7ヶ月くらいで新しい環境になれるのかもしれません。

仕事や人間関係がもともとあまり問題になってないのは、人と直接会わない分ストレスが低いからでしょうか。(もともと人間関係が良いのかもしれない!)

エンジニアのスコアが全社と比較して全体的に悪いのは、エンジニアのほうが環境の変化に敏感だからかもしれません。

こうやって数字にしてみると全体の動きがわかりやすくなるので、パルスサーベイはやってよかったです。

うちはこんなメトリクスとって良かったよ、とかあればぜひ教えてください。

エンジニア4名の早期退職

今年の夏に入社したエンジニアが4名も早期退職(入社後3ヶ月以内)になってしまいました。 退職理由はそれぞれ別だったのですが同時期に複数人が早期退職するのは創業以来初めてだったので、フルリモートが影響してるのだろうと仮説をたて、入社オンボーディングを見直すことにしました。

「なんでも聞いてね」から「聞きにいく」スタイルへ

これまですぐ隣にチームメイトがいたので「なんでも聞いてね」が通用していたし、それで十分でした。しかしリモートになるといくらオンラインで常時つなげていても、ふとしたときに聞くハードルはすごく上がっていて(もしくは慣れない)、思い切ってこっちから「聞きに行く」スタイルにしました。

チームメートとの1on1が大事

入社後なかなかなじめない理由の一つに、チームへの所属感を持ちにくい、なかなかチームメンバーとの距離感が縮まらないというのがありました。これまでも1on1は大事にしていてマネージャーやメンターとの1on1はこまめにやっていましたが、実はチームメンバーとの1on1こそリモートでは大事なんじゃないかという話になりました。

結論、入社1on1しまくる

上の2点をふまえて、入社後3ヶ月は1on1しまくることにしました。 マネージャーやメンターだけでなく、チームメンバーも2週に1度1on1、マネージャーのマネージャーも毎月1on1、入社した人が嫌じゃない限り1on1しています。 ただし1on1は基本15分としました。

僕も入社したエンジニア全員と毎月1on1していて、(彼ら彼女らの役に立っているかは不明ですが)どこにやりにくさを感じるか、いまの環境になにが足りないのかを直接聞けて、すごくいい機会になっています。

いろんなチームビルディング

10ヶ月もリモートワークが続くとリモートランチやリモート飲みだけだとどうしても飽きてきてしまうので、社内で好評だったおもしろい取り組みを紹介します。

  • リモートでボードゲームしながら飲む
  • リモートで毎朝コーヒー飲む時間をつくる(仕事の話禁止)
  • リモートでプルリクみながら飲む
  • リモートでプロダクトKPIの数字を眺めながらご飯食べる
  • プロダクト単位でemg/pm/ux/qaみんなでリモートオフサイトする

特にリモートオフサイトは準備は大変だけどすごく盛り上がったそうです。

人事労務freeeのリモートオフサイトでは、競合他社のプロダクトをみんなで触ったり、1年間のOKRの読み合わせで理解を深めたり、ユーザーの声を動画でもらったり、みっちり5時間盛り上がって参加した人の反応もよかったようです。

意外とプライベートな話題より、がっつり仕事の話の方がみんな共通して関心が高いので、リモートでもチムビルになりやすいのかもしれません。

freeeのこれからの働き方

コロナの感染状況が落ち着く前提で、freeeでは4月から週2出社 + 週3リモートのハイブリッドな働き方にトライする予定です。

いまではすっかりリモートワークに慣れたのですが、出社には出社のいいところがあったので、今後の働き方を決める上で一番むずかしいハイブリッドにトライしてみることにしました。いいところや働きづらいところがたくさんでてくるはずなので、その学びを踏まえて今後のfreeeの働き方を議論していきたいと思っています。

社内で出社したいという声をけっこう聞くようになったり、採用の現場でもフルリモートの会社は避けたいという人もでてくるようになりました。

あらためて出社とリモートを両方経験してみて、なるべくいいとこどりできるような新しい働き方を見つけていきたいです。

スペアリブの作り方

そういえば今日はクリスマスイブなので、クリスマスぽいレシピを載せて終わります。

母のレシピですが、鍋に放り込むだけで激ウマスペアリブの出来上がり。 家族からの評価がうなぎ登ります。 ふだん料理しない自分でもめっちゃ簡単だったので、ぜひ作ってみてください。

オレンジジュースはポンジュースがおすすめ!

f:id:hiraguri-hiraguri:20201223234247j:plain
母のレシピ

  • スペアリブ 1kg
  • オレンジジュース
  • トマトケチャップ 大さじ5
  • しょう油 大さじ3
  • ウスターソース 大さじ2
  • 玉ねぎ
  • 人参
  1. 肉に塩こしょうして全面に焼き色をつける
  2. 鍋に肉を入れ、肉がかぶるくらい迄オレンジジュースを注ぎ、ベイリーフ、トマトケチャップを入れ、煮込む
  3. 途中、人参を加え少し煮込み、しょう油、ウスターソースを加え、煮る
  4. 最後に玉ねぎを入れ更に煮る

f:id:hiraguri-hiraguri:20201223233602j:plain
激ウマスペアリブ

明日は最終回、freeeの創業者CTO @yokoji です!

メリクリ〜よいお年を〜

アドベントカレンダーは、ぎりぎりにならないと書けないを解決する

この記事は freee Developers Advent Calendar の 23 日目の記事です。


悩みの種の泉へようこそ

ここでは皆様から送られてきた悩みの種を ひも(@him0net) が無駄に技術で解決し、どのくらい解決できているかを担当編集者の takuma に評価してもらいます。

それでは、悩みの種を紹介しましょう。

「こんにちは、会計 freee のアプリケーションをエンジニアをしている ひも です。弊社では freee developer アドベントカレンダーというものがあり、12/1 から 12/25 のクリスマスまで、1日1つブログ記事を公開していくという取り組みをしています。私は毎年11月に、この執筆者の募集で立候補してしまうのですが、いざ12月になるとバタバタして全然記事が書けていないことに状態で苦しんでいます。ここで思ったのですが、技術の力で記事の執筆を支援することはできないでしょうか?よろしくおねがいします。」

この悩みの種、技術で解決するとこうなります。

developers ブログの記事をもとに文章の自動生成を行うと「xxx」という文章ができる。

実際に実装してみた

まずは、文章の生成の方法の技術選定を行う。文書の生成プログラムをざっくり調べたところ、Chainer や TensorFlow を用いた RNN (回帰型ニューラルネットワーク)によるものと、マルコフ連鎖に基づくものが見つかった。

時間が無い(超重要)ので、アルゴリズムを理解しているマルコフ連鎖に基づく文章の自動生成を採用することにした。

また freee のアドベントカレンダーの記事を作成することが目的なので、シードデータには過去のアドベントカレンダー (2018-2020) の記事を利用することにする。記事の収集には先進の技術(コピペ)を用いた。

f:id:him0:20201223011411g:plain
アドベントカレンダーをコピペしてシードデータを集めている様子。

マルコフ連鎖の辞書作成にあたり、分かち書きには、形態素解析エンジン MeCab を用いた。セットアップには ipa辞書インストールも必要であった。

python から MeCab が呼び出せる環境を構築を行い、分かち書きを行った。

$ poetry run python wakati.py
['', 'こんにちは', '、', 'freee', '株式会社', 'で', 'エンジニア', 'を', 'やっ', 'て', 'いる', 'id', ':', 'ymrl', 'です', '。', '\n', 'はやい', 'もの', 'で', '、', '2018', '年', 'も', '残す', 'ところ', '1', 'ヶ月', 'と', 'なり', 'まし', 'た', '。', '\n', '12', '月', 'と', 'いえ', 'ば', '年末', '調整', 'と', 'Advent', 'Calendar', 'です', 'ね', '!', 'という', 'わけ', 'で', '、', 'この', '記事', 'は', 'freee', 'Developers', 'Advent', 'Calendar', 'の', '1', '日', '目', 'です', '。', '\n', '今年', 'も', 'また', '12', '月', '25', '日', 'まで', '毎日', 'リレー', 'で', '記事', 'を', '掲載', 'し', 'て', 'いき', 'ます', '。', '\n', 'さて', '、', '今回', 'は', '開発', '者', 'ブログ', '、', 'つまり', ...

分かち書きが正しくできているのか確認すため、単語の出現回数順で表示してみた。

$ poetry run python collection_order.py
('の', 4699)
('、', 4405)
('\n', 3983)
('。', 3968)
('を', 3824)
('て', 3585)
('に', 3415)
('が', 2946)
('は', 2708)
('で', 2459)
('た', 2301)
('し', 2279)
('ます', 1709)
('と', 1603)
('です', 1125)
('こと', 971)
('も', 970)
('い', 898)
('する', 832)
('な', 797)
('まし', 789)
('いる', 599)
('という', 572)
('freee', 561)
...

freee という単語がこの順位ででてくるのはなかなかにこの freee developers blog らしい文章ができそうで期待が高まってきた。

マルコフ連鎖のロジックの実装を行う。階層は適当に決め打ちで3階でいくことにした。3単語をキーにして、次に出てくる単語が決定する。分かち書きを行った単語をキーとその後に出てくる単語のリストと言う形で辞書として格納していく。

dictionary = [
  ([ 'こんにちは', '、', 'freee' ], [ '株式会社', ... ]),
  ([ '、', 'freee', '株式会社' ], [ 'で', ... ]), ...
]

実際の文章を生成するロジックでは最新の3単語は保持して、マルコフ連鎖の辞書から次のワードのリストを決定し、リストの要素からランダムで1つの単語を抽選する。

prev_three = ['こんにちは', '、', 'freee']

# python で、array find するの地味に大変だった
next_word_list =  next(filter(lambda x: (x[0][0] == prev_three[0] and x[0][1] == prev_three[1] and x[0][2] == prev_three[2]), dictionary), None)
next_word = random.choice(next_word_list)

ロジックが出揃った。

実際に出力してみた。

さて、どんな文章が生成されるのか!!!

f:id:him0:20201223135850g:plain
terminal でプログラムを実行している様子

$ poetry run python markov_chain.py
昔からエンジニアリングもマネージメントも好きなので、エンジニアとしての原点回帰であっても、どういった料理が来たかや何をお皿によそったのかを知るために編 み出した方法として、巷では自作キーボード界隈でいうところの終着点的な意味です。

こうしてこの世界でまた1つ悩みの種が技術で解決された。

developers ブログの記事をもとに文章の自動生成を行うと「エンジニアから料理に目覚め、自作キーボードに落ち着く」という文章ができる。

運命の評価発表

それでは、どのくらい解決できたかを評価していただきます。

him0「どうですか?」

takuma「エンジニアとしてのキャリア像について語るかと思いきや実際には料理について考察しているようで、非常に興味深いです。」

takuma「でも、そんなことより、来年は11月から準備してくださいね。」

him0 🥺

him0「明日は、hg さんの記事です。」

him0, takuma 「それでは、またお会いしましょう。でわでわ〜」


自分はマルコフ連鎖の辞書をタプルで実装してしまったが、[Python]N階マルコフ連鎖で文章生成 の deque を使った実装がきれいだったので、コードはこちらを参考にした方が良いです。また、文章の初めに [BOS] を設定し、 を FOS として扱う実装もこちらを参考にさせていただきました。ありがとうございました。

オンラインになった社内イベントをエンジニアも全力で手伝っている話

この記事は freee Developers Advent Calendar の 22 日目の記事になります.

こんにちは, freee で会計freee の開発をしているエンジニアのけむりだま (@_kemuridama) です. 2018 年に新卒入社して, 普段は会計freee の新機能開発や保守運用, 社内デザインシステム Vibes のメンテナンスをしていたりしています.

asobiba スタジオ

フルリモートで社内イベントがオンラインになった

今年は新型コロナウィルス感染症が猛威を奮った激動の一年でした. freee も 3 月から完全フルリモート勤務を行っていて, 自分がリーダーとして主催している freee Tech Night も残念ながらオフラインではなく 4 月からオンラインで実施しています.

freee は「ムーブメントで世界を変える」という目標を組織として掲げていて, ムーブメント型チームを目指すために社内交流をを促進させるための社内イベントも数多く存在しています. これらのイベントもオンラインで実施できるように社内で試行錯誤してきました. 自分はエンジニアとしてそのほとんどに携わってきたのでその紹介をしていきたいなと思います.

New社式 (入社式)

3 月からフルリモートになって初めてのオンラインイベントは 4/1 の New社式でした. もともと自分はこの New社式の企画の時点から携わっていたのですが, 刻一刻と情勢が変わる中で完全にオンラインでやるのか, 新卒だけはオフラインでやるのかなど本当にギリギリまで調整をしていました.

今年は残念ながら完全オンラインとして配信スタジオとなった本社の 9F の asobiba (多目的スペース) にある畳スペースには MC と配信スタッフの 4 人だけという最小構成で新卒を迎えるべく New社式を開催しました.

New社式の asobiba スタジオ
New社式の asobiba スタジオ

カメラで MC の 2 人を撮りつついろんな出演者たちと Google Meet とつないで MC とコミュニケーションできるようにキャプチャーボードを使って OBS で合成しつつ社内 SNS の Workplace で配信を行いました. このときはまだ社内にも配信するための機材が十分になくて自分もいくつか機材を提供したりして, なんとか無事に配信することができました.

New社式の詳しい中身に関しては 【 New社式 】オンラインで新卒27名の入社式を開催しました! を見てください!!

freee SPIRIT 2021

次に自分が携わってきたのが年に 1 度の全社合宿の freee Spirit (通称フリスピ) です. こちらも本来であれば大きな会場を借りて開催をしているのですが, 今年はオンラインになりました. フリスピは登壇者も多く, かつ社員同士がグループに分かれてディスカッションする必要があるので Web 会議サービスの Remo を使って配信しました.

フリスピでは「DS (CEO) を自宅からオンラインで中継 & クロマキーを使って合成する」という試みと「ワカルさん (社内に存在するゆるキャラ) をバーチャル合成する」という試みを新たに行いました.

freee SPIRIT 2020 の asobiba スタジオ
freee SPIRIT 2020 の asobiba スタジオ

前述の DS の中継に関しては自分が担当していました. 事前に DS の自宅にロケハンに向かい, 中継安定のために有線 LAN が中継場所となる部屋に引けるだろうかとかクロマキーのためのグリーンバックがちゃんと設置できるかなど確認したり, 当日も朝早くから集合して中継や asobiba スタジオにいる MC と合成ができるかなど調整をしました. DS の映像の他に登壇スライドも asobiba に送らないといけなかったり, asobiba にいる MC と DS がコミュニケーションできるようにしないといけなかったり, かなり色々とハックしてやり遂げました.

DS の自宅から中継する様子
DS の自宅から中継する様子

後述のワカルさんのバーチャル合成は UX の id:ymrl がもともとあった絵からモデルを Live2D でサクッと作ってくれて, VT-4 で声を変えつつ Web カメラでモデルと動きを Sync できるようなシステムを作ってくれました.

ワカルさん合成システム
ワカルさん合成システム

フリスピの詳しい内容については「社員500名 全員オンライン!8時間の大規模イベントを気合とハックでやりきった話。」を見てください!!

株主総会

こちらは社内イベントではないですが, 自分が関わったイベントかつ freee らしい新しい取り組みだったと思うので紹介させてください. freee が上場後初めての株主総会はコロナ禍ということもあって, オフラインとオンラインで同時開催するハイブリッド株主総会にすることになりました.

株主総会の配信スタジオ
株主総会の配信スタジオ

株主総会はオフラインでやっている内容をそのまま YouTube Live で配信するという形でした. そのため, 喋る可能性のある人数が多かったのでマイクの台数こそ多かったものの配信システムはカメラとミキサーと ATEM mini という単純な構成で配信することができました. スライドはカメラを通して映すと見えづらくなったので ATEM mini で合成しました.

大忘年会

先週末に開催された freee大忘年会ももちろんオンライン開催です. asobiba の一角をスタジオに変えて毎週全社会議で司会をやっている 2 人を MC に迎えてカメラとミキサーと ATEM mini と OBS でスタジオを構築し, Remo で配信を行いました.

大忘年会の配信スタジオ
大忘年会の配信スタジオ

今回の肝は 12 拠点からの生中継でした. 12 人が各自宅から Google Meet に入ってそれをスタジオでスイッチングしながら配信するという挑戦をしました. 自分はオフラインとオンラインの映像のスイッチングすべてを担当しました. どのタイミングでどれにスイッチすればいいのか映像ソースが多いので混乱したりしましたがなんとか無事に配信することができました.

おわりに

今回は自分の携わってきた freee の社内オンラインイベント運営について紹介しました. 自分を含めた社内のエンジニアの何人かはよくわからないガジェットをたくさん持っていたり, 家に配信システムがあったりする人がいるので, それらの知識を最大限に発揮して試行錯誤しながら毎回 1 からお手製で配信スタジオを構築しています.

生配信はミスやトラブルが起きればそのまま流れてしまって緊張しますが, やり遂げたときの達成感や参加者からの感想を聞いているとやっていて楽しくなってきます. また社内イベントの配信クオリティを高めることは自分との戦いとなる部分も大きいので, 毎回前回の自分を超えられるように…って思ってます.

こんな感じで色々なものを Hack Everything★ して様々な課題を乗り越えていける仲間を freee では募集してます. コードを書いてプロダクトの成長に貢献する以外にも自分ができることを最大限発揮して freee のカルチャーに貢献してくれるような人が来てくれたら嬉しいです.

jobs.freee.co.jp

明日は一緒に社内イベントの配信をやっているひもくんです. お楽しみに!!!

ミドルウェアのソースコードリーディングのすすめ

この記事は freee Developers Advent Calendar 2020 の 21 日目です。

プロダクト基盤本部で本部長をしています浅羽と申します。プロダクト基盤は文字通りプロダクトの基盤を作っており、SRE、アカウントアグリゲーション基盤、セキュリティ、アプリケーションの基盤、品質、Eng企画と多岐にわたるチームになっています。普段は組織運営であったり困ったことに相談乗ったり、暇を見つけてコード書いたりしています。よろしくおねがいします!

freeeではサービスの安定稼働を重視しており、SREではデプロイの高速化やカナリアリリースなどのような「仮にまずいリリースがでてもすぐに引っ込める」仕組みの開発をしています。もちろんサービス障害が発生しないようにQAチーム中心に対策していますが、失敗はある前提の元でいかにリカバリを素早くできるかも大切にしています。これはこれでどこかで記事にしたいですが、今回は別の話です。

障害が発生しないように対策していますが、それでもサービスの進化とともに課題も生まれてきて予期せぬサービス障害を引き起こすケースがまれに発生します。障害の根本原因はさまざまです。そこで、サービス障害が発生したらなぜそれが発生したのだろう?というのを振り返り、組織的な課題や技術的な課題を見つけて改善していきます。

社内では「なぜなぜ分析」と呼び、社内で定義した障害レベルに応じて「なぜ?」とN階層まで深ぼっていきます。経験的には3階層くらい深堀ると徐々に本質的な課題が見えてくるので、発生した現象に対して「なぜ?」を3階層以上問い続けるのをオススメします。これはこれでどこかで記事にしたいですが、今回は別の話です。

サービス障害の原因の一つとしてミドルウェア起因の障害があるかと思います。超意訳で「ちゃんとドキュメントを読んで、ちゃんと使いましょう」みたいな振り返りで解決するケースもありますが、ミドルウェアの挙動をしっかり追いかけて特性を理解することは結構大事だと思っています。また障害に限らず、新しいミドルウェアを導入する際にある程度検証するかと思いますが、検証しているなかでどうしても不可解な挙動に悩まされることは経験ある方もいるかと思います。なので、色々学びがある(それ以上に楽しい!)のでミドルウェアのソースコードを一緒に読んでみましょう、というのを社内でたまにやります。いくつかのミドルウェアのソースコードリーディングの事例を紹介したいと思います。

なお、この記事で取り上げているソースコードのバージョンが古いものもありますが、過去に調べたものを使っているためです。現時点で使っているバージョンではないのでその点はご承知おきください。

ソースコードリーディングの準備

大抵のミドルウェアのコードはそこそこ大きいです。先頭から読み始めるとすぐに挫折するので興味あるところだけに絞るとよいです。例えば「xxの機能が想定しない挙動をした」という場合は、xxの機能のエントリポイントを見つけてそこだけを深ぼっていきます(これはミドルウェアに限った話ではないですね)。過去の経験からこんな感じで読んだり、読む準備をすると良いんじゃないかなというのをご紹介します。

デバッグビルドとデバッガで止める方法を探す

エディタだけでコードを読むのは結構大変です。動かしながらコード追いかけるのをおすすめします。

  • 大抵の場合はデバッグビルドをするためのオプションが用意されているので、configureを動かす前にオプションを探す
  • C/C++の場合、コンパイラの最適化が走らないようにオプションを制御します(CFLAGSで無理やり-O0を渡すとか)
    • 最適化が効いた状態でデバッガを起動するとレジスタに書かれたりして変数のダンプが大変になります
  • 勇気を持って必要ないところ(もしくは細かすぎる挙動のところ)は読まない、というのも大事
  • データ構造等は紙とペンつかって書き出すのも大事

あると便利ツール

コマンド名 用途
lsof 開いているファイルやソケットを調べる
procfs プロセスの状態を色々調べたいときにみる
netstat, ss ネットワークの接続状況などを調べる
tcpdump 何送っているんだろう?と調べたい場合。暗号化されたら見てもわからない
ipcs shared memoryを調べたい場合(めったに使わない)
strace システムコールをトレース

知っておくとあたりを付けやすい

  • 読みたい機能に関連したシステムコールからbreakpointを貼ることで目的の場所に早く行くことができるので、システムコール名と役割はなんとなく知っていると良い
  • サーバー実装のテクニック
    • self pipe trick
    • Cの場合はメモリプール
    • daemon処理
    • マルチスレッド、マルチプロセス(都度forkする、pre-fork)
    • などなどあるが、いくつかコードを読むと「あ、このテクニックどこかでもみたことある」と気づけるはず

ではいくつかやっていきましょう。

nginx

まだALBが存在していない時期になりますが、とある社内のサービスでCLBの前段にnginxを置きたい要件がありました。nginxがinternal CLBにproxy passするという設定を入れていたのですが、CLBのインスタンスが入れ替わってIPアドレスも変わった場合に、接続タイムアウトが頻発する問題が発生しました。挙動的にCNAMEの結果をTTL無視してキャッシュしているんだろうなと想像したのですが、念のため確認することにしました。ちなみに最新のコードでどのような挙動になっているかは確認していません。ぜひ確認してみてください。

debug build

1.11.12ですが最新版でも恐らく同じような方法です。

wget https://github.com/nginx/nginx/archive/release-1.11.12.tar.gz
tar zxvf release-1.11.12.tar.gz
cd nginx-release-1.11.12
sudo apt-get install gdb emacs make libzip-dev
./auto/configure --prefix=$HOME/nginx --with-debug --without-http_rewrite_module
make install

起動

こんな感じなのを設定します。resolverはresolv.confにかいてあるアドレスを書いてください。

events {
  worker_connections  4096;
}

http {
  server {
    listen 8800;
    location / {
      resolver xx.xx.xx.xx valid=30s; # ここは自宅の環境等のresolv.confを調べてください
      resolver_timeout 3s;
      proxy_pass http://www.freee.co.jp; # これは適当
    }
  }
}

以下のコマンドでnginxを立ち上げます。親プロセスと子プロセスが一個立ち上がります。子プロセスが実際のworkerです。

% ~/nginx/sbin/nginx
% ps auwwfx (MacOSのpsだと f オプションないです) で、子プロセスのPIDをメモってください。
% gdb -p 上のpid

breakpoint

proxy passするということは、とうぜんconnect(2)をどこかで呼び出しているはずなので、まずはソースコードをざっくり検索します。

 % grep -r 'connect(' .
./event/ngx_event_openssl_stapling.c:static void ngx_ssl_ocsp_connect(ngx_ssl_ocsp_ctx_t *ctx);
./event/ngx_event_openssl_stapling.c:    ngx_ssl_ocsp_connect(ctx);
...
./http/ngx_http_upstream.c:static void ngx_http_upstream_connect(ngx_http_request_t *r,
./http/ngx_http_upstream.c:static ngx_int_t ngx_http_upstream_test_connect(ngx_connection_t *c);
./http/ngx_http_upstream.c:            ngx_http_upstream_connect(r, u);
./http/ngx_http_upstream.c:    ngx_http_upstream_connect(r, u);
./http/ngx_http_upstream.c:    ngx_http_upstream_connect(r, u);
./http/ngx_http_upstream.c:ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u)
./http/ngx_http_upstream.c:    if (ngx_http_upstream_test_connect(c) != NGX_OK) {
./http/ngx_http_upstream.c:    if (!u->request_sent && ngx_http_upstream_test_connect(c) != NGX_OK) {
./http/ngx_http_upstream.c:    if (!u->request_sent && ngx_http_upstream_test_connect(c) != NGX_OK) {
./http/ngx_http_upstream.c:ngx_http_upstream_test_connect(ngx_connection_t *c)
./http/ngx_http_upstream.c:                                    "kevent() reported that connect() failed");
./http/ngx_http_upstream.c:            (void) ngx_connection_error(c, err, "connect() failed");
./http/ngx_http_upstream.c:    ngx_http_upstream_connect(r, u);
...

なんとなくupstreamに対してconnectしている↑のあたりが怪しいと推測できるので、片っ端からbreakpointを貼っていきます。

  • 読みたい処理が決まったら、多分通るんだろうなと思う関数を見つけてbreakpointを貼ってみる
  • 止まらなかった場合は別にbreakpointを貼る
    • 止まったらbacktraceを bt コマンドで取る
  • debuggerだけでなく、他のツールも組み合わせて読む
    • 今回の場合はupstreamへのconnect(2)のタイミングを知りたかった
  • gdbでstep実行しつつ、netstat -ntで、www.freee.co.jpに接続する(ESTABLISHED)タイミングをみつける
  • primitiveなところまでたどり着いたら、今度は逆に抽象度を上げていって、必要なさそうな箇所はすっとばす
  • いい具合で調査を切り上げる(はまってそのまま読み続けてもok)

proxy passの挙動

コードを読んで見るとproxy_passの名前解決は2つ挙動があることがわかりました。

  • proxy_passに直接proxy先を書いてある場合は、nginx起動時にgethostbyname()をやって、TTL無視してそのまま再利用する(いままでの設定)
  • proxy先を一旦変数に入れてやると、resolverの設定を使って自前でDNSに問い合わせる
DNS lookupを自前でする箇所
  • gethostbyname()を使わずに、nginxのソースコード内でDNS queryを組み立ててUDP(or TCP)で指定したDNS serverへ送る
  • DNSへ送るデータのフォーマットはRFC1035を見てみてください
(gdb) bt
#0  ngx_resolve_name (ctx=0xc72570) at src/core/ngx_resolver.c:412
#1  0x00000000004573bc in ngx_http_upstream_init_request (r=r@entry=0xc78730) at src/http/ngx_http_upstream.c:750
#2  0x0000000000457d02 in ngx_http_upstream_init (r=r@entry=0xc78730) at src/http/ngx_http_upstream.c:532
#3  0x000000000044af39 in ngx_http_read_client_request_body (r=r@entry=0xc78730,
    post_handler=0x457c1f <ngx_http_upstream_init>) at src/http/ngx_http_request_body.c:84
#4  0x0000000000478054 in ngx_http_proxy_handler (r=0xc78730) at src/http/modules/ngx_http_proxy_module.c:929
#5  0x000000000043e22c in ngx_http_core_content_phase (r=0xc78730, ph=<optimized out>)
    at src/http/ngx_http_core_module.c:1386
#6  0x0000000000438f33 in ngx_http_core_run_phases (r=r@entry=0xc78730) at src/http/ngx_http_core_module.c:860
#7  0x0000000000439042 in ngx_http_handler (r=r@entry=0xc78730) at src/http/ngx_http_core_module.c:843
#8  0x0000000000441540 in ngx_http_process_request (r=r@entry=0xc78730) at src/http/ngx_http_request.c:1921
#9  0x00000000004441f2 in ngx_http_process_request_headers (rev=rev@entry=0x7fa63bdd20d0)
    at src/http/ngx_http_request.c:1348
#10 0x0000000000444507 in ngx_http_process_request_line (rev=rev@entry=0x7fa63bdd20d0)
    at src/http/ngx_http_request.c:1028
#11 0x0000000000444f1c in ngx_http_wait_request_handler (rev=0x7fa63bdd20d0) at src/http/ngx_http_request.c:503
#12 0x0000000000435b66 in ngx_epoll_process_events (cycle=0xc72f10, timer=<optimized out>, flags=<optimized out>)
    at src/event/modules/ngx_epoll_module.c:902
#13 0x000000000042bc3a in ngx_process_events_and_timers (cycle=cycle@entry=0xc72f10) at src/event/ngx_event.c:242
#14 0x0000000000433630 in ngx_worker_process_cycle (cycle=0xc72f10, data=<optimized out>)
    at src/os/unix/ngx_process_cycle.c:749
#15 0x0000000000431f92 in ngx_spawn_process (cycle=cycle@entry=0xc72f10,
    proc=proc@entry=0x43359d <ngx_worker_process_cycle>, data=data@entry=0x0,
    name=name@entry=0x48b72f "worker process", respawn=respawn@entry=-3) at src/os/unix/ngx_process.c:198
#16 0x0000000000433795 in ngx_start_worker_processes (cycle=cycle@entry=0xc72f10, n=1, type=type@entry=-3)
    at src/os/unix/ngx_process_cycle.c:358
#17 0x0000000000434226 in ngx_master_process_cycle (cycle=cycle@entry=0xc72f10) at src/os/unix/ngx_process_cycle.c:130
#18 0x000000000040c854 in main (argc=<optimized out>, argv=<optimized out>) at src/core/nginx.c:375
upstreamにconnect(2)するところ
(gdb) bt
#0  ngx_event_connect_peer (pc=pc@entry=0xc79720) at src/event/ngx_event_connect.c:172
#1  0x0000000000456265 in ngx_http_upstream_connect (r=r@entry=0xc78730, u=u@entry=0xc79710)
    at src/http/ngx_http_upstream.c:1485
#2  0x000000000045778d in ngx_http_upstream_resolve_handler (ctx=0xc72570) at src/http/ngx_http_upstream.c:1210
#3  0x000000000042683a in ngx_resolver_process_a (ans=<optimized out>, trunc=<optimized out>, nan=<optimized out>,
    qtype=<optimized out>, code=<optimized out>, ident=<optimized out>, n=<optimized out>, buf=0x7fff6eabf670 "",
    r=0xc8b3a0) at src/core/ngx_resolver.c:2440
#4  ngx_resolver_process_response (r=0xc8b3a0, buf=buf@entry=0x7fff6eabf750 "}\223\201\200", n=<optimized out>,
    tcp=tcp@entry=0) at src/core/ngx_resolver.c:1844
#5  0x0000000000427d68 in ngx_resolver_udp_read (rev=0x7fa63bdd2130) at src/core/ngx_resolver.c:1585
#6  0x0000000000435b66 in ngx_epoll_process_events (cycle=0xc72f10, timer=<optimized out>, flags=<optimized out>)
    at src/event/modules/ngx_epoll_module.c:902
#7  0x000000000042bc3a in ngx_process_events_and_timers (cycle=cycle@entry=0xc72f10) at src/event/ngx_event.c:242
#8  0x0000000000433630 in ngx_worker_process_cycle (cycle=0xc72f10, data=<optimized out>)
    at src/os/unix/ngx_process_cycle.c:749
#9  0x0000000000431f92 in ngx_spawn_process (cycle=cycle@entry=0xc72f10,
    proc=proc@entry=0x43359d <ngx_worker_process_cycle>, data=data@entry=0x0,
    name=name@entry=0x48b72f "worker process", respawn=respawn@entry=-3) at src/os/unix/ngx_process.c:198
#10 0x0000000000433795 in ngx_start_worker_processes (cycle=cycle@entry=0xc72f10, n=1, type=type@entry=-3)
    at src/os/unix/ngx_process_cycle.c:358
#11 0x0000000000434226 in ngx_master_process_cycle (cycle=cycle@entry=0xc72f10) at src/os/unix/ngx_process_cycle.c:130
#12 0x000000000040c854 in main (argc=<optimized out>, argv=<optimized out>) at src/core/nginx.c:375

なので、当時の回避策として以下のような設定をしました。

events {
  worker_connections  4096;
}

http {
  server {
    listen 8800;
    location / {
      resolver xx.xx.xx.xx valid=30s;
      resolver_timeout 3s;
      set $server "www.freee.co.jp"; #これは適当に
      proxy_pass http://$server;
    }
  }
}

unicorn

unicornはRubyで書かれたRackアプリケーションサーバーです。freeeで使っています。unicornは以下の特徴を持っています。

  • multi processでpre fork型
  • hot restartができる

このhot restartは実際にどういう挙動で、本番に入れる際にどういうリスクがありそうか調べることにしました。

debug準備

ダミーアプリの準備

unicornを動かすためにrailsアプリを生成します。

bundle exec rails new . -B -d mysql --skip-turbolinks --skip-test

Gemfileに以下の行を追加してbundle installします。

gem 'pry'
gem 'pry-remote'
gem 'pry-byebug'
gem 'unicorn'
config/unicorn.rb

こんな感じのを用意します。

worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3)
timeout 15
preload_app true

remote_pryを仕込む

bundle installしてインストールしたunicornのソースコードを直接編集します。例えばSIGUSR2の挙動を調べたい場合はhttp.rbの以下のあたりのコードを追加します。

@@ -305,6 +305,8 @@
         logger.info "master done reopening logs"
         soft_kill_each_worker(:USR1)
       when :USR2 # exec binary, stay alive in case something went wrong
+        require 'pry-remote'
+        binding.remote_pry
         reexec
       when :WINCH
         if $stdin.tty?

修正したらunicornを立ち上げます。

% bundle exec unicorn_rails -c config/unicorn.rb -E development

リモートデバッグする

ps auwwfxで親プロセスのPIDを探し、 kill -USR2 pid でシグナルを送ります。そうするとunicorn側で以下のようなログが出ます。

[pry-remote] Waiting for client on druby://127.0.0.1:9876

でたら、remote debugの準備で以下のコマンドを叩きます。

 % bundle exec pry-remote

そうするとunicornを起動したコンソールがdebug可能状態になります。

[pry-remote] Client received, starting remote session
[pry-remote] Remote session terminated
[pry-remote] Ensure stop service

From: /home/y-asaba/dev/testapp/vendor/bundle/ruby/2.4.0/gems/pry-remote-0.1.8/lib/pry-remote.rb @ line 321 Object#remote_pry:

    319: def remote_pry(host = PryRemote::DefaultHost, port = PryRemote::DefaultPort, options = {})
    320:   PryRemote::Server.new(self, host, port, options).run
 => 321: end

[1] pry(#<Binding>)>

あとはお好きにどうぞ。

Redis

O(n)の処理を呼んでしまったためにパフォーマンスが劣化した障害がありました。振り返り後に社内で勉強会を開催しました。Cで書かれておりだいたいnginxと同じような感じで読みすすめることができます。細かい手順は以下のスライドに書いてあるのでそちらをご覧ください。

speakerdeck.com

環境作るのが面倒な場合はDockerfileも用意してありますのでご自由にお使いください。

github.com

MySQL

freeeではMySQLを使っています。instant add columnのニュースが来たときは「これは使えれば最高だが本当に大丈夫?」と思い、2018年のadvent calendarで書きました。(裏advent calendarなので公式blogではないです)

y-asaba.hatenablog.com

MySQLはこのなかで一番巨大なので、ソースコードを見る前にRDBMSのなんとなくの構造を先に理解しておくと捗るかと思います。コードを見つつ以下の資料を参考にしてみてください。

おわりに

デバッガを使うことで動かしながらコードを読む方法をいくつか紹介しました。大抵の挙動はドキュメントに書かれていることが多いですが、細かいところはコード見ないとようわからんというケースも一方では僅かではありますがありますので、今回紹介した手法を用いてぜひミドルウェアのソースコードリーディングを楽しんでみてください!不具合を見つけてPull Requestも送ってあげれば喜ばれると思います。

次回は会計freeeのフロントエンドからバックエンドまで開発しまくっているkemuridama氏です。お楽しみに!