右上↗

プログラミングに関するメモをのこしていきます

いろんな問題を「意思の問題」と捉えない方がいい

チームで働いていると、いろんな問題が起きる。 いろんな問題が起きると、原因を特定して対処する必要がある。

ここで、意思が原因であると捉えてしまうといろいろと辛くなる。 自分自身に対して、意思が原因であると内省するのは構わないと思う。自分は割とよくある。 ここでは、チームで抱えている課題を、チームで解決したい状況について考えている。

そもそも大体の場合、意思の問題ではない。 自分も含めみんな各々勉強しているし、世のベストプラクティスも知っていて、「こうした方がいいよね」とはみんな思っていることが多い。 それでも解決していないのは、意思がないからじゃなくて、そうできない理由があるからだ。 たとえば「どうやったらいいかわからない」「忙しくて余裕がない」「もっと効果的な別のアクションがあると思っている」とか。

もちろん、確固たる意思を持って「できない理由」を吹き飛ばして実行できる人がいるのは事実だと思う。 ただそれはその人のスペシャリティであって、一般に「意思があればできる」と仮定するのは無理がある。

また、ある問題が意思の問題であるとなった瞬間に、解決までにかかるコストがあがり、時間もかかるようになる。 なぜなら、人の意思を変えるというのは事実としてそんなに簡単じゃないし、みんなそう思っているからこそ慎重になりやすく、アグレッシブな打ち手を取りづらくなるから。 意思の問題と捉えるのは、そこに問題を押し付けるといろんな問題が解決しそうに見えやすくてイージーな手だけど、実際には1番解決が難しい捉え方だったりする。

ではどうするべきか。 基本的な話だけど、仕組みで解決しよう。

みんなやりたいと思っているのに実行されないのは構造的な理由があるのではないか? 意思に頼らずに解決できるような仕組みを作れないか?

仕組みで解決するのは、人を動かすより簡単で、しかも実行しやすい。失敗してもやり直せばいいだけだ。

フレームワークを作れる人に憧れながら、ユーティリティを作る

エンジニアとして働くにあたって、無意識に気をつけていたことを自覚するたびに、言語化しておきたいなと思うようになりました。 今日はフレームワークを作らないようにしている話を書いてみます。 (個人ブログを育てていたのですが、なんとなくはてなブログに復帰することを検討しているので、こっちに書いてみる)

なぜフレームワークを作らないのか

フレームワークの定義は正直いまだに厳密にはよくわかっていませんが、ここでは「その仕組みの上に乗っかってプロダクトコードが書かれるような基盤となる仕組み」全般をフレームワークと呼びます。(Reactは自称libraryですが、このブログではあれはフレームワーク扱いしたい、くらいのふわっとした独自定義でいきます。そもそもグラデーションなので、あまり厳密なものでもありません。)

なぜ僕がフレームワークを作らないようにしているかというと、すごく単純な話で、自分でフレームワークを作るには能力が足りていないと思っているからです。フレームワークを作るのは、非常に難易度が高いのです。もう少し正確に言うと、「フレームワークを作って、それを継続的にチームで運用し続けること」の難易度が非常に高いと思っています。

「運用」という言葉もニュアンスが広いのですが、ここでの「運用」には、そのフレームワークが(当初の思想を理解したうえで)正しい方向に進化していくための改善活動も含んでいます。

オレオレフレームワークに苦しめられる、みたいなのはあるある話だと思います。自分自身はそこまでひどく苦しんだ経験はありませんが、類似のケースには何度か触れてきました。

フレームワークには思想が必要

フレームワークには思想が必要です。思想なきフレームワークは、すぐに崩壊するものです。

フレームワークと呼べるくらい “仕組み” になっているものは、フレームワークの内部で解決される、暗黙(ユーザーから見て)の挙動をそれなりに含んでいます。

思想の伝わらないフレームワークは、ユーザーから見ると、ブラックボックスの中で何が起きているか・何をやってくれているのかを想像することが難しくなります。

本当にブラックボックスとして扱って構わない場合は、ブラックボックスの中で何が起きているかをユーザーが知る必要はないのですが、思想が伝わらないフレームワークだと、ユーザーは「フレームワークに何を任せたらいいか」の判断が適切にできなくなります。

結果として、本来フレームワークを拡張することで解決すべき問題をユーザーランドで無理矢理に解決してしまったり、逆にユーザーランドで解決すべき問題をフレームワークの中に持ち込んでしまったりします。前者はフレームワークの恩恵がどんどん薄れていくことを意味するし、後者はフレームワークの複雑さを招き、この問題をより加速させる結果に繋がります。

自作フレームワークを正しく運用するために必要なもの

フレームワークが正しく運用されるには、以下のどちらか(あるいは両方)が必要です。

  1. フレームワークの思想と正しい運用方法を普及する活動にコストを割く
  2. 運用が自走するくらい直感的に動く & 思想が簡単に浸透するフレームワークを作る

1. フレームワークの思想と正しい運用方法を普及する活動にコストを割く

「1. フレームワークの思想と正しい運用方法を普及する活動にコストを割く」は、その重要性は言わずもがなです。

ドキュメンテーションやオフィスアワーの開催など、いろいろな打ち手が考えられます。

ここで重要なのは、自作フレームワークを作った場合、これをやるのは作者しかいないということです。

世間一般で使われているフレームワークであれば、公式のドキュメントを頼ることもできますし、世間にあふれるユーザーの声を頼ることもできます。

社内に閉じた自作フレームワークの場合は、これができません。

作者自身がまず利用者になるだけでなく、伝道師になる必要があるのです。

詰まったときにググっても何も出てこない自前フレームワーク v.s. ググればいくらでも事例が出てくるOSS、というイメージです。(実際にはそこまで自明な状況で自作フレームワークを作ることは現実ではあまりないと思いますが)

また、先に述べたように、フレームワークには思想が必要です。チームでメンテナンスする対象としてのフレームワークという文脈においては、さらにその思想をちゃんとチーム内に伝導することが非常に大切です。どのような思想に基づいて足場が設計されているのか、を知らない(または勘違いした)状態で、足場の”改善”をしようとすれば、より脆い足場が作られることになります。

思想の伝導には、単に使い方レベルの説明をするだけでは足りません。ADRのように、アーキテクチャ上の意思決定の履歴を残す取り組みが普及してきていますが、フレームワークについても同様に「どういう状況下で、どういう課題を、どう解こうとしたからこういうフレームワークになったのだ」ということが伝わるようにしなければなりません。

誰もついてこれない思想なら、その思想はそのチームにはフィットしていないし、そのフレームワークはチームにとってプラスではありません。ついてこられるように伝導するコストを払いましょう。

(ここでは、フレームワーク自体の「実装が難しくて」作者にしかメンテナンスできない、という問題は無視しています。それは単にチームの技術力不足であり、ここで取り上げたい問題とは別のものです。往々にして、メンテナンスできなくなる要因は、実装が高度なことではありません。)

当然のことながら、これらのコストを払う価値があると判断している場合は、話は別です。

例えば、一般的な解法ではプロダクトの提供したいレベルのUXがどうしても実現できず、そのUXを提供することこそがプロダクトの競争優位になる、と判断している場合などです。

その場合は、ドキュメンテーションやそのメンテナンスコストを支払うことを前提に、フレームワークを作りにいくべきです。

(一方で、そんなケースは非常に稀だろうとも思っています。)

2. 運用が自走するくらい直感的に動く & 思想が簡単に浸透するフレームワークを作る

「2. 運用が自走するくらい直感的に動く & 思想が簡単に浸透するフレームワークを作る」も非常に重要で、「フレームワークを作れるに人に憧れながら」と題してこのブログを書いているのは、確かにこれができる人というのは存在するし、目の当たりにしてきたからでもあります。

絶妙な薄さ、絶妙な API、絶妙な柔軟性と規約のバランスなどが噛み合っているときしか達成し得ないことです。

Example を見ただけて直感的にどういうことをしてくれるフレームワークか伝わる、やりたいことのほとんどがフレームワークによって考慮済みである、といった条件が整えば、そのフレームワークはどんどん使われていくことになるだろうし、フレームワーク自体にガタが来ることも少ない(大量の変化を必要としない、健全に進化する)はずです。

ただし、これは明らかに難易度が高いです。スキル・経験・センスすべてがものをいう世界だと思います。

後ろ向きな発言ですが、僕はまだこれに自信と責任を持つことはできません。

これらの2つの理由から、フレームワークを作ることは避けるようにしよう、というのが僕のエンジニアとして心がけていることの一つです。

では効率化を試みることは悪なのか

当然ながらそんなことはありません。僕は以下の二点を心に留めるようにしています。

フレームワークなんて作らなくても効率化はできる

フレームワークと呼べない程度の、小さな Utility / Helper の集合を作ることは、どう転んでも害になりにくいものです。

勘所が悪いと、想像より益をうまない Utility を作ってしまうかもしれませんが、そういうものがあったとしても、多くの場合は、少しコードサイズが膨らむだけですみます。

Utility 程度であれば、ドキュメンテーションもそこまで頑張る必要はありません。普通に KDoc なり TSDoc なりを書いておけば十分です。

Example も、自動テストとして残しておけばよいので、コストは掛かりません。

小さいに Utility そのものに思想はそこまで現れません。(これは少し嘘かもしれません。強い思想がにじみ出る Utility というものは存在しますね。 Kotlin の let  なんかは個人的には思想が現れている Utility の一例に感じます。)

さらにいえば、小さな Utility を合成して積み重ねていくことによって、実質的にフレームワークと呼べるような機構に育つこともあるかもしれません。

このケースは、ある意味では危険信号(局所最適に陥っている可能性)かもしれませんが、理解しやすいブロックの合成で構築されたフレームワークは、それ自体が理解しやすいものになりやすいはずです。少なくとも中をブラックボックスとして扱わなくていい程度には。

挑戦をしないとフレームワークが作れるようにはならない

さっきはフレームワークを作らないようにしていると書きましたが、僕も一ソフトウェアエンジニアとして、スーパー使いやすくて生産性の高い “フレームワーク” と呼ぶにふさわしい仕組みを作ってみたいと常々思っています。

最初からきゅぴーんと閃いてフレームワークを作れる人もいるのかもしれませんが、小さい成功体験を積み上げてパターンの引き出しを増やすことで、少しずつフレームワークを作れるエンジニアになっていくというパスが自分には向いているのかなと思っています。

自分が過去に見てきた “仕組みを作れる人” たちも、最初から完璧なものを作っていたわけではなさそうでした。小さく作ってFeedbackを受けながら育てていたり、一気に作って個人で利用する中で反芻して改善したものをリリースしていたり(当たり前ですが)

何にせよ、そういう経験を踏まなければパターンの引き出しが増えず、仕組み化できるものを仕組み化するスキルがないままになってしまいます。自分の許容ラインを少し超えるくらいのアグレッシブな仕組み化に挑戦し続けていきたいなと思います。

まとめ

大仰な仕組みを作っても、チームがついてこられないと不幸を呼ぶので、

  • チームがついてこられるように布教活動をきちんとやる
  • チームがついてきやすいよう、思想や実装がわかりやすい筋のいい仕組みを作る

の二点に気をつけましょう。

そしてそれらをきちんとやるのは非常に難しいことを理解し、大仰な仕組みを作らなくて済む解決方法(Utilityを提供する程度に留めるなど)も検討しましょう。

ただし、巨大だけど筋のいい仕組みを作れるソフトウェアエンジニアになるためには、その領域に挑戦することが必要なので、バランスを見つつ挑戦していきましょう。

2023年ふりかえり

2023年が終わるので、ふりかえり。

仕事

株式会社ヘンリーで引き続きエンジニアをしている。今年もやることがちらほらかわって飽きない良い環境だった。

ロール

ヘンリーに入社して2年が経ちました で書いた通り、チームリードをやっていました。

今年は、チームリードといいつつなんでもやる的なロールから、PjM に比重を移し、さらにその後はチームのマネジメント業務から離れて Tech Lead 的なロールに移行した一年だった。 自分は穴の空いた部分を埋めるムーブをするのがどうも好きらしく、ヘンリーに入ってから何度もチーム移籍やロール変更を繰り返しているが、今年はとにかく「自分よりこの領域について明らかにスキルが高い人」が入社してくれたことによるロール移行ばかりで、とても喜ばしかった。

事業成長にコミットするという観点では、「自分よりもスキルが高い人が入社してくれること」「その人がより得意なことをやってくれること」はとても大事だし、ポジションに固執することなくリードを渡せたことは良かった。 一方で、自分がちゃんとバリューを出せるように、自分のスキルを磨くことや価値を出せる場所を探すことも大事だと思う。 来年は、遊撃手的な動きを強めつつチームのあいだの溝を埋めていくことと、それをやりながら飛び道具や仕込み刀を用意していくことに注力していけると良いな、と思っている。

明らかに Project Management よりも開発そのものにコミットしたほうが価値が出せるなぁというのが、今年の自分の中での大きな気づきだった。 もちろん開発で大きいインパクトを生み出すためには Project Management 含め、マネジメント能力が一定求められるとは思っている。 得意ではないなと自覚しつつ、致命的に苦手というほどでもないとは思っているので、開発力を強みにして価値を出しながら、マネジメント能力も磨いていきたい。

チーム

tyamagucdamが10月に入社してくれて、それをきっかけにチームがめちゃくちゃ良い状態になった。 それぞれ PjM, PdM を担ってくれていて、自分から業務を引き継いだ。

僕がプレイングで中途半端にやっていた世界とはまるで違っていて、ふりかえりやStand Upミーティングのやり方がガンガン改善されていき、圧倒的に状態が良くなっているのを日々実感している。 ここまで変わるのかーーという悔しさをいだきつつ、おかげで気兼ねなく開発に集中することができているし、開発力で返さないとなぁと思っている。

もちろん元からいたメンバーは、それぞれの得意な領域があって活躍していたけど、きちんとしたチームになれたことでそれがより正しく発揮されるようなチームになってきた気がしている。 得意な領域が混ざり合うような体験が増えてきて、お互いの得意領域に引っ張られてカバー範囲が広がっていくような実感がある。

僕のやっていた頃と比べてなにが違うのか、具体的に言葉にするのは難しいのだが、ぱっと思いつく範囲では

  • アイディアを実行する力が強くなった
    • ふりかえりなどで出たアイディアが確実に実行される。僕がやっていたころは漏れまくっていた。
  • リードできる人が増えたことで、定例ミーティングのファシリがしっかりした&やりやすくなった
    • リードできる人は、自分自身がファシリテータでないときも、ファシリに乗っかってサポートしたりできるので、ファシリがやりやすい
  • ベストプラクティスとされていることをちゃんとやるようになった
    • 元々ベストプラクティスはこうだよね〜という会話はあったのだが、どうしても実行力が弱かったために、徹底しきれず、破綻していた
    • 加えて、実行に時間を取れない事がわかっていた状況だったので、破綻することもわかっており、その結果、最初から緩めの設計でやろうとしてしまっている節があった

こうかくと凡事徹底としかいいようがないのだが、これがきちんとできるチームはどれだけあるだろうか、とも思うので、幸せな環境にいることを噛み締めたい。

ISUCON

いい加減勝ちたいのだが、今年も3位相当スコアからのfailという残念な結果に... (ISUCON13に「都営三田線」で参加して 227,377 → failしました!)

今回はfailの原因がわかって(僕のせいでした!!)、反省ができたので、次は勝てるはず!!! 都営三田線というチームでISUCONに出始めたのがISUCON7で、そのときからずっとかなりいい線を行っている...はず... (ISUCON 7 本戦出場してきました 「都営三田線東急目黒線直通急行日吉行」)

あとISUCONをやると、今のメンバーと一時的に一緒に仕事をするみたいな体験ができるのだけど、それが自分の成長にとってもめちゃくちゃよい。

@0gajun は、準備がめちゃくちゃ良い。一つ一つのしごとの完成度が高く、完全に任せて一切の不安がない。 また、インフラを主に担当してもらっている都合上、中盤から終盤にかけてアプリ側を担当しているメンバーと情報格差が生まれがちだが、キャッチアップが速いのですぐ追いついてくる。 アプリ側が焦っている場合には自己判断で勝手にアプリ側に関わる修正をしてくれたりと、とにかく頼りになる。

@izumin5210 は、圧倒的な実装力はもちろんなのだが、個人的にはあの極限状態のなかで書いてくるコードが毎度毎度ふつうにキレイなことに驚いている。 僕も割とコードを読むのは速い方だし、手を打つべき場所を特定したり、実際に実装を修正したりするのはそれなりに得意だと思っているのだけど、コードの質の一点だけは明らかにレベルが違うなと感じている。 書いているコード量や普段からの意識が生み出す瞬発力なんだろうなぁと思って見ていて、自分の振る舞いを反省するばかり。 普段の業務において、そんなにやっつけのコードばっかり書いているわけではないと思うんだけど、毎回魂込めて設計しているかといわれると...胸に手を当てて見つめ直したい。

自分は正直ハードスキルですごく誇れるものはないと思っていて、雑食つまみ食いでここまで来てしまっているのだけど、ISUCONだけはハードスキルの証明として誇りに思っているし大事にしたいと思っている節がある。 そろそろ優勝したいなぁ。

おわり

ほかにも家庭のこととか色々あるけど、外向けに所信表明がてら書きたいことだけを書いてみた。 今年は家庭も仕事もとても充実していて楽しかったし、久々に成長の実感があった良い一年だった!来年もがんばりたい。

PostgreSQLでカラムの型や制約を取得する方法

jooq のように、RDBスキーマからコードを生成するツールをほしいシーンがあった。 「こういうテーブルがあって、そこにはこういう名前のカラムがあって、そのカラムの型はこうで、制約はこうだ」という情報をつかってコードを生成したい。 DDLをパースするという手もありそうだったが、PostgreSQLに限定すればもっと簡単にできそうだった。

以下のようなクエリを実行するだけ。(ここでは例として users テーブルが存在すると仮定している)

SELECT
    c.table_name,
    c.column_name,
    c.data_type,
    e.data_type AS element_type,
    c.is_nullable,
    c.character_maximum_length
FROM
    information_schema.columns c
LEFT JOIN information_schema.element_types e
        ON ((c.table_catalog, c.table_schema, c.table_name, 'TABLE', c.dtd_identifier)
        = (e.object_catalog, e.object_schema, e.object_name, e.object_type, e.collection_type_identifier))
WHERE
    c.table_name = 'users'
ORDER BY
    c.table_name, c.ordinal_position

data_type には character varying のような文字列が入っている。 PostgreSQL には array 型があるが、その場合は data_typearray が入っていて、element_typecharacter varying が入っている。 また、character varying のような型には character_maximum_length が入っている。( text 型の場合は 0 になる)

ただし、この方法だと型は文字列として取得されるので、文字列比較で処理を書く必要があり、若干不安な気持ちになる。(まぁこの文字列表現に非互換な変更をいれてくることは想像し難いので気持ちの問題だけど)
PostgreSQL には oid というのがあって、そっちを使うほうが安心かもしれない。同じようなクエリで oid も取得できる。

実際、Rust 製の DB アクセスライブラリである sqlx では、この oid を使って型を判定している。
https://github.com/launchbadge/sqlx/blob/929af41745a9434ae83417dcf2571685cecca6f0/sqlx-postgres/src/type_info.rs#L250-L357

外部キー制約などの constraints も information_schema.table_constraints などを使うことで取得できそうだが、今回の自分の用途では不要だったので詳しく調べていない。

また、今回はテーブルとそのカラムに関する情報を取り扱うことに限定されていたが、先述の sqlx や sqlc のように、ユーザが書いたクエリに対して型安全なコードを生成するライブラリも最近は流行ってきている。 与えられたクエリ(プレースホルダーあり)に対して、プレースホルダーに与えるべきパラメータの型や結果の型情報を取得することで、型安全なコードを生成することができる。 こういったことを実現するためには、PostgreSQL通信プロトコルを用いて、PARSE コマンドなどを発行し、型情報を取得するようだ。 (https://github.com/launchbadge/sqlx/blob/0c8fe729ff1d4e67bbd211209c691f2816ab6f02/sqlx-postgres/src/connection/executor.rs#L23) こっちのパターンも Rust でちょっと実験してみたので、別の記事に書くかもしれない。

とりあえず、なんにせよ「与えられたPostgreSQLのデータベースに対して、型安全なコードを生成する」というマジックをどのようにやっているのか知れたので、勉強になった。意外とやれば簡単に作れそう。
実用するとなると PostgreSQL だけというわけにはいかないだろうから、他のデータベースでも同じようなことができるかどうかも調べてみたいと思ったのと、うまく抽象化する設計力が求められそうだなと思った。

ISUCON13に「都営三田線」で参加して 227,377 → failしました!

ISUCON13 お疲れ様でした! 今年も「都営三田線東急目黒線直通急行日吉行」というチームで参加しました。 チーム編成は 7 年連続(?)同じメンバーで、0gajunizumin の 3 人チームでした。

最終スコア 227,377 点からの fail という猛烈に悔しい結果に終わりました...!! ISUCON11は本戦出場からの本戦で3位相当スコアを取りながらfailしたのですが、今回も全く同じで3位相当のスコアを取りながらまたしてもfailしてしまいました...

実はISUCON12予選もギリギリ本戦行けるかどうか微妙なラインで結局failするという結果に終わっていて、3年連続でfailという残念すぎる結果に... そんなにギリギリをついて攻めたことをしているつもりは無いのですが、なぜかいつもfailしてしまいます。 今回はfailの原因もわかった(後述)し、反省できる内容だったので、しっかり反省を活かしたい。

幻の3位
failしている様子

ISUCON13 受賞チームおよび全チームスコア : ISUCON公式Blog

やったこと

言語は例年どおりのGoを選択しました。(僕個人は業務でGoを使っていないのもあって、generics時代のGoにまともに触れるのは実は初めてだったのですが、普通に無茶苦茶便利でした。) また、今回はDNS周りは正直僕はほとんど触れないまま完走してしまったので、解像度が低いです。

いつもどおり初手で適当にIndexを貼ってまわって、10:23頃に7,282点で暫定一位を取って記念撮影をしました。

index連打で暫定一位

そのあとは

  • アプリケーションサイド:
    • 愚直にN+1を潰す
    • statistics 系のエンドポイントを効率化するために、逐次でスコアを計算しDBに書いておく
    • iconの配信で304を返せるようにする
  • インフラサイド:
    • PowerDNSの前段にdnsdistを置いてキャッシュを効かせたりRate Limitをかけたり

というのを進めていきました。 PowerDNS側のMySQLがCPUをもっていってしまうので2台目に分けたり、ある程度最適化してもアプリケーション側のMySQLボトルネックになっていることが明らかになってきたので3台目にわけたり、というのも並行して進めていきました。

前半、13:00ごろまではなにをやっても負荷が上がりきらず8000点位をウロウロしていたのですが、 dnsdist がはいったあたりでようやく1つ壁を超えて、32,588点まで伸びました。(14:19ごろ)

DNS側がうまくさばけていないことで、ベンチマーカーがあまりリクエストを送ってくれていなかったのか、このへんは @0gajun による DNS 周りの調整でガンガンスコアが伸びていきました。 Rate Limit を調整して 77,601 点までいったのが 15:24 ごろ。

そろそろ構成を Fix しようということで、

  • 1台目: nginx + PowerDNS + app (weight = 3)
  • 2台目: MySQL (PowerDNS) + app (weight = 5)
  • 3台目: MySQL (app)

構成に移行し、115,353 点。(15:47ごろ)

ログを切ったり、残った N+1 を潰したりという細かい改善を進めていってじわじわスコアを伸ばしました。

もう限界そうだなぁとなってきたタイミングで、キャッシュを入れることを検討し始めました。 livestream は作成されたら二度と更新されないし、user も icon の更新以外はなかったので、キャッシュしやすく、かつどちらも comment や reaction 系のエンドポイントの中でも構築する必要があるオブジェクトだったため、キャッシュの効果も大きいだろう、と考えました。

2台構成だったので、

  • オンメモリにデータを持ちつつ、RDBにはちゃんとデータを書き込む。キャッシュミスが起きればRDBからデータを取得してオンメモリに乗せる。
  • Redisに載せる。

の2パターンで、izuminと僕で分担しつつ実装を進めました。 このあたりがうまくハマって、最終的には 227,377 点まで伸びました。(17:30ごろ)

我々のチームの最終盤の alp のログと MySQL の slow query log はこんな感じでした。

+--------+-----+-------+-------+-----+-----+--------+---------------------------------------------+-------+-------+---------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
| COUNT  | 1XX |  2XX  |  3XX  | 4XX | 5XX | METHOD |                     URI                     |  MIN  |  MAX  |   SUM   |  AVG  |  P90  |  P95  |  P99  | STDDEV | MIN(BODY) | MAX(BODY)  |   SUM(BODY)   | AVG(BODY) |
+--------+-----+-------+-------+-----+-----+--------+---------------------------------------------+-------+-------+---------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
| 108135 | 0   | 10028 | 98102 | 5   | 0   | GET    | /api/user/[\w\d]+/icon                      | 0.004 | 0.064 | 122.808 | 0.001 | 0.004 | 0.004 | 0.008 | 0.002  | 0.000     | 103167.000 | 77882124.000  | 720.230   |
| 11290  | 0   | 11286 | 0     | 4   | 0   | POST   | /api/livestream/\d+/livecomment$            | 0.004 | 0.232 | 101.548 | 0.009 | 0.016 | 0.020 | 0.028 | 0.007  | 0.000     | 1767.000   | 17303791.000  | 1532.665  |
| 1043   | 0   | 1040  | 0     | 2   | 1   | POST   | /api/register                               | 0.004 | 0.256 | 85.464  | 0.082 | 0.112 | 0.124 | 0.156 | 0.022  | 0.000     | 482.000    | 447785.000    | 429.324   |
| 10826  | 0   | 10826 | 0     | 0   | 0   | GET    | /api/livestream/\d+/reaction                | 0.004 | 0.096 | 82.436  | 0.008 | 0.012 | 0.016 | 0.028 | 0.006  | 3.000     | 161885.000 | 223926384.000 | 20684.129 |
| 10795  | 0   | 10795 | 0     | 0   | 0   | GET    | /api/livestream/\d+/livecomment$            | 0.004 | 0.088 | 76.152  | 0.007 | 0.012 | 0.016 | 0.024 | 0.005  | 5.000     | 171215.000 | 249985236.000 | 23157.502 |
| 9864   | 0   | 9864  | 0     | 0   | 0   | POST   | /api/livestream/\d+/reaction                | 0.004 | 0.224 | 70.476  | 0.007 | 0.012 | 0.016 | 0.024 | 0.006  | 1331.000  | 1611.000   | 14461030.000  | 1466.041  |
| 5453   | 0   | 5453  | 0     | 0   | 0   | GET    | /api/livestream/search                      | 0.004 | 0.084 | 59.760  | 0.011 | 0.016 | 0.024 | 0.036 | 0.007  | 40453.000 | 156745.000 | 267346886.000 | 49027.487 |
| 1285   | 0   | 1275  | 0     | 10  | 0   | POST   | /api/livestream/reservation                 | 0.008 | 0.248 | 36.760  | 0.029 | 0.040 | 0.044 | 0.056 | 0.012  | 57.000    | 1091.000   | 1215036.000   | 945.553   |
| 7033   | 0   | 7032  | 0     | 1   | 0   | GET    | /api/livestream/\d+/report                  | 0.000 | 0.032 | 31.296  | 0.004 | 0.008 | 0.012 | 0.016 | 0.003  | 0.000     | 3977.000   | 208899.000    | 29.703    |
| 3434   | 0   | 3434  | 0     | 0   | 0   | POST   | /api/livestream/\d+/moderate                | 0.004 | 0.208 | 21.532  | 0.006 | 0.012 | 0.012 | 0.020 | 0.006  | 18.000    | 18.000     | 61812.000     | 18.000    |
| 5251   | 0   | 5251  | 0     | 0   | 0   | GET    | /api/livestream/\d+/ngwords                 | 0.004 | 0.028 | 20.828  | 0.004 | 0.008 | 0.008 | 0.016 | 0.003  | 5.000     | 1460.000   | 1254619.000   | 238.930   |
| 1162   | 0   | 1119  | 0     | 0   | 43  | POST   | /api/icon                                   | 0.004 | 0.240 | 18.012  | 0.016 | 0.024 | 0.032 | 0.052 | 0.011  | 9.000     | 149.000    | 18771.000     | 16.154    |
| 2213   | 0   | 2212  | 0     | 1   | 0   | GET    | /api/livestream                             | 0.000 | 0.028 | 9.036   | 0.004 | 0.008 | 0.008 | 0.016 | 0.003  | 0.000     | 19075.000  | 5623652.000   | 2541.189  |
| 1360   | 0   | 1359  | 0     | 1   | 0   | GET    | /api/tag                                    | 0.004 | 0.036 | 5.668   | 0.004 | 0.008 | 0.008 | 0.016 | 0.003  | 0.000     | 3402.000   | 4623318.000   | 3399.499  |
| 1048   | 0   | 1046  | 0     | 2   | 0   | POST   | /api/login                                  | 0.004 | 0.020 | 5.116   | 0.005 | 0.008 | 0.012 | 0.016 | 0.003  | 0.000     | 59.000     | 118.000       | 0.113     |
| 1      | 0   | 1     | 0     | 0   | 0   | POST   | /api/initialize                             | 4.608 | 4.608 | 4.608   | 4.608 | 4.608 | 4.608 | 4.608 | 0.000  | 22.000    | 22.000     | 22.000        | 22.000    |
| 96     | 0   | 96    | 0     | 0   | 0   | GET    | /api/livestream/\d+/statistics              | 0.008 | 0.080 | 4.340   | 0.045 | 0.064 | 0.072 | 0.080 | 0.014  | 79.000    | 84.000     | 7921.000      | 82.510    |
| 945    | 0   | 945   | 0     | 0   | 0   | POST   | /api/livestream/\d+/enter                   | 0.004 | 0.172 | 3.904   | 0.004 | 0.008 | 0.008 | 0.016 | 0.008  | 0.000     | 0.000      | 0.000         | 0.000     |
| 936    | 0   | 936   | 0     | 0   | 0   | DELETE | /api/livestream/\d+/exit                    | 0.000 | 0.024 | 3.696   | 0.004 | 0.008 | 0.012 | 0.016 | 0.003  | 0.000     | 0.000      | 0.000         | 0.000     |
| 96     | 0   | 96    | 0     | 0   | 0   | GET    | /api/user/[\w\d]+/statistics                | 0.008 | 0.080 | 2.780   | 0.029 | 0.048 | 0.052 | 0.080 | 0.013  | 108.000   | 142.000    | 11569.000     | 120.510   |
| 62     | 0   | 60    | 0     | 2   | 0   | POST   | /api/livestream/\d+/livecomment/\d+/report$ | 0.004 | 0.024 | 0.528   | 0.009 | 0.012 | 0.016 | 0.024 | 0.004  | 52.000    | 2148.000   | 120814.000    | 1948.613  |
| 94     | 0   | 94    | 0     | 0   | 0   | GET    | /api/user/[\w\d]+/livestream                | 0.008 | 0.012 | 0.488   | 0.005 | 0.008 | 0.012 | 0.012 | 0.003  | 949.000   | 19068.000  | 581533.000    | 6186.521  |
| 95     | 0   | 95    | 0     | 0   | 0   | GET    | /api/user/[\w\d]+/theme                     | 0.004 | 0.012 | 0.392   | 0.004 | 0.008 | 0.012 | 0.012 | 0.003  | 29.000    | 29.000     | 2755.000      | 29.000    |
| 3      | 0   | 3     | 0     | 0   | 0   | GET    | /api/user/me                                | 0.004 | 0.004 | 0.008   | 0.003 | 0.004 | 0.004 | 0.004 | 0.002  | 195.000   | 207.000    | 609.000       | 203.000   |
| 1      | 0   | 1     | 0     | 0   | 0   | GET    | /api/user/test                              | 0.000 | 0.000 | 0.000   | 0.000 | 0.000 | 0.000 | 0.000 | 0.000  | 195.000   | 195.000    | 195.000       | 195.000   |
| 1      | 0   | 1     | 0     | 0   | 0   | GET    | /api/livestream/\d+$                        | 0.000 | 0.000 | 0.000   | 0.000 | 0.000 | 0.000 | 0.000 | 0.000  | 1091.000  | 1091.000   | 1091.000      | 1091.000  |
| 1      | 0   | 1     | 0     | 0   | 0   | GET    | /api/payment                                | 0.000 | 0.000 | 0.000   | 0.000 | 0.000 | 0.000 | 0.000 | 0.000  | 16.000    | 16.000     | 16.000        | 16.000    |
+--------+-----+-------+-------+-----+-----+--------+---------------------------------------------+-------+-------+---------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
Count: 1283  Time=0.02s (19s)  Lock=0.00s (0s)  Rows=10.1 (12972), isucon[isucon]@2hosts
  SELECT * FROM reservation_slots WHERE start_at >= N AND end_at <= N FOR UPDATE
Count: 74228  Time=0.00s (8s)  Lock=0.00s (0s)  Rows=0.0 (0), isucon[isucon]@2hosts
  COMMIT
Count: 1162  Time=0.01s (6s)  Lock=0.00s (0s)  Rows=0.0 (0), isucon[isucon]@2hosts
  INSERT INTO icons (user_id, image, sha256) VALUES (N, _binary'S', 'S')
Count: 31976  Time=0.00s (4s)  Lock=0.00s (0s)  Rows=1.0 (31976), isucon[isucon]@2hosts
  SELECT * FROM livestreams WHERE id IN (N)
Count: 3514  Time=0.00s (3s)  Lock=0.00s (0s)  Rows=50.0 (175700), isucon[isucon]@2hosts
  SELECT user_id, sha256 FROM icons WHERE user_id IN (N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N)
Count: 96  Time=0.04s (3s)  Lock=0.00s (0s)  Rows=1.0 (96), isucon[isucon]@2hosts
  SELECT rank_
  FROM (
  SELECT id, RANK() OVER (ORDER BY score DESC) AS rank_
  FROM livestreams
  ) AS ranked_users
  WHERE id = N
Count: 3514  Time=0.00s (3s)  Lock=0.00s (0s)  Rows=50.0 (175700), isucon[isucon]@2hosts
  SELECT * FROM themes WHERE user_id IN (N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N)
Count: 11287  Time=0.00s (2s)  Lock=0.00s (0s)  Rows=0.0 (0), isucon[isucon]@2hosts
  INSERT INTO livecomments (user_id, livestream_id, comment, tip, created_at) VALUES (N, N, 'S', N, N)
Count: 9851  Time=0.00s (2s)  Lock=0.00s (0s)  Rows=16.1 (158932), isucon[isucon]@2hosts
  SELECT * FROM livecomments WHERE livestream_id = N ORDER BY created_at DESC
Count: 9883  Time=0.00s (2s)  Lock=0.00s (0s)  Rows=15.1 (149160), isucon[isucon]@2hosts
  SELECT * FROM reactions WHERE livestream_id = N ORDER BY created_at DESC
...

最終盤にきても CPU が 7 割くらいしか使い切れず、なにがボトルネックなのかいまいち分からなかったので、「スコアは上がらないけど速くなっている/負荷が下がっている」みたいな変更を最終盤にガンガン取り込んだのですが、そのなかの一つが fail の原因でした...

fail の原因

さきほどの slow query を見ると、一番上が reservation_slots のロックを取るクエリになっています。 これが遅いせいで CPU を使い切れていない / リクエスト速度が上がり切っていないのでは?と考え、このロックを最小化する変更をいれようとしました。 ref

処理としては、

    1. reservation_slots を SELECT FOR UPDATE で抑える
    1. 枠が空いていれば、reservation_slots の残枠を減らす
    1. livestream を INSERT する
    1. livestream_tags を INSERT する
    1. 作った livestream を JSON で返すために、関連データも合わせて DB から取得する

ということを1トランザクションの中で行っている部分です。

ここで、少なくとも 5 は transaction をコミットしたあとにやっても問題ないはず、ということで、5 を transaction の外に出す変更をすでに入れていました。 追加で最終盤にもうちょっと攻めようとして、3, 4も transaction の外に出すというのをやりました。(17:42ごろ) 3, 4は(アプリケーションレイヤでは)失敗し得ない処理なので、transaction の外に出しても問題ないはずだと考えました。 実際これをやってもベンチは通っていて、スコアは微増しました。(223,455 → 226.609)

しかし、ここで僕がトランザクションの分け方をミスっていて、3 と 4 を atomic にすることに失敗していました。 livestream はあるのに livestream_tags がない、という状況が瞬間的に発生しており、その刹那を観測されると正しくないデータが返ってしまう状態になっていました。

さらに悪いことに、livestreamをオンメモリでキャッシュしていたので、この状態が発生すると、livestream_tags がない状態の livestream がキャッシュに乗ってしまい、その後のリクエストにも影響を与えてしまう、という状態になっていました。 livestreamには更新系がない、というのも災いして、キャッシュをinvalidateするコードを書きそびれており、 3, 4の間に別のリクエストのせいでキャッシュが暖まってしまう可能性を考慮していませんでした。

今思えばスコア的には大したゲインもないわりに攻めすぎたのですが、あのときは NaruseJun チームの異常なスコアにビビっていたのもあり、勝つためにはもっと攻めなければという意識になっていました。 もっと余裕のあるスコアを取れていれば、このあたりの攻めはしなかったかもしれませんが、今回は終了1時間前の時点で拮抗していたし、実際蓋を開けてみれば 220,000 点あたりが割と詰まっていたので、攻めないという判断は難しかったなぁとも思います。

まとめ

というわけで ISUCON13 は僕のミスによる fail で終わってしまいました。申し訳ない...

もともと負け惜しみいっぱいの感情でこのブログを書いていたのですが、昨日出題チームのrepositoryが公開され、(ref) failの理由が判明してしまったので、シンプルに負けたなという気持ちです。 くやしい!!!!!!! そろそろ勝ちたい!!!!!

出題に関しても、ボリュームたっぷりで、とっつきやすいところから攻めていけるようになっていて、とても楽しかったです。 インフラ的な面白さもぐっと増していて、いい勉強にもなりました!(まだ正直インフラまわりの変更は理解できていないので、ふりかえりして勉強したい) 運営の皆様、楽しいイベントありがとうございました!

Nord Theme を使い始めた

Nord を使い始めた。

昔は color scheme を変えて気分転換する、というのはとても頻繁にやっていて、自分の中ではお手軽リフレッシュのネタだった。 が、iTerm2, VSCode, Vim, IntelliJ, etc... と、いろんなツールを使っていると、色が揃わない気持ち悪さがあって、コロコロ変えるのも面倒だったり、そもそもその色合いが特定のツールで(簡単に)使えるかどうかも微妙だったりして、おそらく 8 年くらいは icebergを使い続けていた。

今回たまたま別の調べ物をしていて WezTerm に移行してみている を見つけ、その中に Nord への言及があったので、チラ見してみたら、かなり好みの色合いだった。 しかもだいぶいろんなツール郡に普及している雰囲気があったので、久々に重い腰を上げて気分転換をやってみた。

ツールのカバレッジは非常に高く、Nord - Portsをみると対応ツールを一覧できる。 自分が今回変えたのは、

Obsidian のテーマは結構悩ましかったところだったので、今回の Nord で定着するといいなと思っている。

VS Code については、コメントの foreground color とワードのハイライトの background color が似すぎていて見辛くなってしまっていたので、以下の様な設定を settings.json に加えてちょっとだけ色をいじった。

    "workbench.colorCustomizations": {
        "[Nord]": {
            "editor.wordHighlightBackground": "#404859"
        }
    },

色は適当だけど、まぁまぁ悪くない。 しばらく使ってみる。 気に入ったらこのブログの色合いも寄せてもいいかもしれない。

(それはそれとして、当初の目的だった WezTerm も試してみたい)

ヘンリーに入社して2年が経ちました

株式会社ヘンリー(レセコン一体型クラウド電子カルテサービスを主力として医療 DX に取り組んでいるスタートアップ)に転職して早くも 2 年が経過しました。 当初は、入社エントリー記事を書くつもりでしたが、あっという間に 2 年が過ぎてしまったため、この節目に振り返りを書いてみようと思います。 あとで自分で見返すために書くので、背景を省いたりしていて、人に見せる記事としてはイマイチだとは思うのですが、書かない・出さないよりいいかなと思い、ここに書いておきます。

入社直後

Henry 開発開始のタイミングでも副業で関わらせてもらっていたりもしたので、入社直後からガンガン開発に携わることになりました。 当時は人数も少なく、メンバーに何名か元同僚がいたというのもあって、すんなり開発に入り込むことはできていたのではないかなと思います。

一方で、電子カルテ・レセコンの開発というのは、入社前に自分が想像していたよりはるかに難解なドメインの塊でした。(レセコンとかそもそも名前すら聞いたことなかったし) この頃に書いたコードは、何一つわかっていないなかで脳死で書いていたもので、いまだに自分が当時書いたコードによって苦しめられたりしています。

当時は、「すんなり開発に入ることこそが自分のやるべきこと」だと信じていたので、無理矢理に溶け込んで走り出すことを目指したのですが、今思えばこれはあまり良くなかった。 きちんと「何を作っているのか」理解していないと、生まれた瞬間から負債になるコードを書いてしまうので、自分のやるべきことは「一つずつしっかりドメインを理解すること」だったと思います。 (といっても当時はそれなりに理解した気持ちになっていたので難しい。)

一方で、 よくわからないなりにひたすら開発に首を突っ込み続けたことが、今でも財産になっている というのも事実です。 電子カルテ・レセコンという、医療現場の ERP であるシステムは、そのカバー範囲が膨大かつ複雑なので、全体をうっすら理解することすらなかなかに難しいと感じています。 当時のまだそこまで巨大でなかったヘンリーのプロダクトやコードベースの時点である程度脳内にマップを作れたことが、いま開発を進めるにあたって有利に働いていると思います。

これは完全に既得権益で、あんまりいいことではないですね。チームで開発を進めていくにあたっては、新しい人が入ってきてもすぐに開発に参加できるように、ドキュメントやコードを整備していくことが重要だと思います。

結局一年くらいはひたすらに開発をし続ける時間だったように思います。あまり覚えていないのですが。 厚労省の資料を熟読してサービスとしての仕様に落とし込むとか、外部機器との連携のために実際に医療機関様の現場にお邪魔してセットアップするとか、転職前では想像もつかないような経験を積む一年でした。

チーム制への移行

入社後一年ほど経ったあたりで、開発チームをいくつかに分けて、それぞれのチームが独立して開発を進めるようになりました。 僕はそのうちの電子カルテ改修チームに所属し、入院版の臨床機能の開発や既存機能のブラッシュアップのためのバックエンド開発を主に担当するようになりました。 この辺はもうちょっとごちゃごちゃとしていたのですが、細かいのと混乱していたのとであまり覚えていないですね...

弊社の開発しているサービスはかなりドメイン知識が必要な分、開発者の知識を移転したりチームを移動したりするフットワークが重くなりがちでした。 僕自身はあまりその壁を感じていなかったのと、薄く広く速くキャッチアップすることには一定の自信があったので、首を突っ込みまくった結果、「もう一方のチームの方もなんとなく分かりつつ、メインでは電子カルテ改修チームに所属している」みたいな状態になっていきました。 なんとなくでも両方のチームのことを把握できている人は多くなかったので、チーム間に落ちる話になると大体出ていくことになり、より一層「両方のチームにまたがる知識」が僕を始めとするチームメンバーに集中するようになっていきました。 (逆に、深いドメイン知識を必要とする部分は、そのドメイン知識を持っている人にしか触ることができず、そちらはそちらで塹壕の中で仕事をしている状態になっていきました。)

これも今思えば罠でしたが、当時の自分はバリューを出せている感を持っていたので、あまり気にしていませんでした。 本当は属人的にチーム間を埋めるムーブをし続けるより、仕組みや構造を改善するべきだったのだろうなと振り返ると思います。

そして育休へ

2022.09 に第一子が誕生し、一ヶ月育休を取得しました 🎉👶 なかなか開発スケジュールがヘビーな中ではあったので、快く送り出してくれたみんなには感謝しています。

戻ってきて

育休から明けて戻ってみると、チーム間連携が以前よりうまくいっていない雰囲気を感じました。 一度離れることで俯瞰視点を得られたというのもあったと思います。

状況的に仕方のなかった面はありつつ、どうにかしないと開発的にも人的にも良くないだろうと感じ、相談の上で僕はもう一方のチームに移籍したうえでチームリードのロールを務めることになりました。 そもそもこの状況に陥らせてしまったこと自体が、全体的に間を埋めるムーブを個人としてやり続けてしまったことの功罪だったかなぁとは思うところですが。 リードといってもチーム間コミュニケーションのインターフェース、くらいの感覚で最初は始めました。

これはすごくいい経験になりました。 改めて開発チームをリードするためにきちんと学ぼうと思い、いろいろ本を読んで得た知識を実践してみたりすることができました。 これはチームメンバーに恵まれたというのも非常に大きいです。何かを僕が提案したとき、大体の場合は「やってみましょう」といってくれたし、本気で懸念がある時はきちんとそれを伝えてくれて議論することができました。

とはいえ、チームをリードするというのはあまりうまくできていなかったなぁと思います。 難易度を上げる要素はいくつかあって、

  • 8 名くらいのチームで、タスクの種類もバラバラだったので、密に連携するにはちょっと大きめだった
  • エンジニアだけでなくドメインエキスパートも所属していた
  • 開発状況が逼迫しすぎていて、自分自身も一人分の開発タスクをまるっと持っていた
  • それでもやりたいことに対してリソースが不足している状況だった

という言い訳はあるのですが、ここで少しでも状況打破にプラスに働くようなアクションをとってやり切ることができなかったのは、自分の責任であり反省ポイントだと思っています。

また、みんな自分より社会人経験が長いメンバーで、やり方や考え方も違う中で、うまくチームの目線や方向性を揃えることができず、意思決定や開発の速度を上げ切ることができませんでした。 「会社がどう考えていてチームはどう考えるべきだ、と僕が考えているか」を伝えきれていなかった。

この頃読んだ本の中では、「エラスティックリーダーシップ」が一番印象に残っています。 当時かなりのサバイバル状況だったので、この本を読んで「サバイバルモードでのリーダーシップ」というものを意識するようになりました。 チームに対しても「今がサバイバルモードであること」を伝えて(といってもみんな感じていたことだけど)、共通認識をとり、ガッとものを進めるムーブを取ろうとしたりしました。

印象に残っているポイントとしては、「ドメインが複雑すぎて、チケットの内容を理解するのにかかるコストと、実装するのにかかるコストのバランスが異常に悪い」ということです。 誇張なしに、3 時間かけて理解して 5 分で実装する、みたいなものがありました。 そうすると PjM として僕がチケットの内容を確認し、誰かにアサインをしようとする場合に、非常にムダが多くなる。僕がブリーフィングをするにも、追加で 1h かかりますとなったらやはりムダが多くなる。

しかし、今から振り返ると、このコストは払うべきでした。そうでないと、メンバーのドメイン知識の向上機会を僕が奪ってしまうことになるし、それによって一生メンバーに任せられない問題が生まれ続けてしまう。 当時は情報をパスするオーバーヘッドすら許容できない状況だと感じていたので、僕しか把握していない情報が増えてきてしまい、メンバーに助けてもらうことすらできない状況を作ってしまいました。 (これは僕目線での文章ですが、おそらくチームメンバーもみな同じようなことをお互いに思っていたのではないかと思います。申し訳ない。)

最近は

そんなこんなでバタバタと斧を研がずに木を切り続けてきた日々でしたが、3 月に大きなマイルストーンを一旦達成したことで、今は少し落ち着く時間をとることができています。 (これはまぁ嘘で、状況的に落ち着いたわけではないのですが、斧を研がないと死ぬということを理解して時間を頑張って確保しようとしているところです。)

数ヶ月前に、チーム間に落ちる・またがるものが多すぎて限界なので、チーム体制を見直しませんか、という話をしてみたところ、いろいろあって開発組織を丸っと再考することができました。 @Songmu が入社してくれたり、2023.04 から新たに超強力な PdM が入社してくれることになったり、開発を進める中でドメイン知識やプロダクトに対する洞察が深まってきたりと、状況が大きく変わっているので、それを反映しよう、という試みです。

2023.04 から新しい体制に移行し、2 チーム制、各チームに PdM が一人ずつ、PjM が一人ずつ、という体制になり、僕自身は PjM として動くことになりました。 僕は PdM 業は全然できていなかったので、助けてもらうことができて正直ホッとしています。(いきなり負荷をかけてしまって申し訳なくはあるのですが)

これまでの反省を活かして、新しい体制では、「誰かが個人として動くことでしかものを解決できない」という状況を是正して、仕組みやチームで解決できるようにするというのをやっていきたいと思っています。

自分にとって、今の環境はかなり恵まれています。 まず、チーム内に心から頼れる PdM がいて、僕の PjM ムーブをサポートしてくれています。 本来僕がやるべきことが抜けている時、サポートしてくれるので全体の進行はスムーズになり、かつ「これは本来自分がやるべきだった」と感じることができる、という学習には最高の環境です。 また、隣のチームでは僕と同じような役割を @Songmu が担っています。隣のチームのことは透明性高く見えているので、これまた「こういうことをやるといいのか」というのを真横で学ぶことができます。 これは本当にありがたいことなので、しっかり学んで身につけていきたい。

まとめ

というわけで、今の僕の状況をまとめると、こんな感じです。 二年間、正直ハードではあったけど、少しずつ良くできていると実感し始めているのと、今の環境は自分の成長にとっても恵まれているので、踏ん張ってきて良かったなぁと感じています。

一方で、まだまだプロダクトの課題は多いです。 また、チームとしても走り始めたばかりなので、これから先実際にどこまでうまくいくのか、どこまで成長できるのか、というのはまだまだ未知数です。 これからも頑張るぞ。