unless’s blog

日々のちょっとした技術的なことの羅列

いまさらだけどTeam Topologiesについてまとめてみる

たまたま仕事でTeam Topologiesをまとめる機会があったので、備忘録がてらブログにしておく

Team Topologiesとは

Matthew SkeltonとManuel Paisによる本

https://amzn.asia/d/f0l0NBR

DevOpsの視点から高速なDeliveryを実現するためにどのようなチームや組織を作るべきかをまとめてる本

チームをDeliveryの最も重要な単位として(Team first-thinking)、チームのパフォーマンスが最大になるようにチームの人数やその責任の範囲の作り方(Team API)から、基本的なチームタイプ(Fundamental team topology)やそのチーム間のコミュニケーション(Team interaction mode)が紹介されてる

コンウェイの法則

システムを設計する組織は、そのコミュニケーション構造をそっくりまねた構造の設計を生み出してしまう

コンウェイ戦略

コンウェイの法則に逆らわず、 理想のアーキテクチャを実現するためにそれにあった組織やチーム構造にする

Team first-thinking

小さく長期的に安定したチームを作ることが非常に重要

小さいチーム

小さくの単位は具体的には5-9人
この根拠はDunbar's number
これは人間が安定的な社会関係を維持できるとされてる人数の認知的な上限
チームの人数が増えるとコミュニケーションのパスの数が増える

Untitled

https://blog.nuclino.com/two-pizza-teams-the-science-behind-jeff-bezos-rule

認知負荷

チームの責任範囲をチームが扱える認知負荷に合わせたものにする
認知負荷を超えたチームは集団志向ではなく個人志向で振る舞うようになる

Ownership

また、チームがOwnershipを持てるようにする
プロダクトの目的の維持とそのための継続的な運用を考えられる
複数のチームが同一のシステムやサブシステムに修正を許すとだれもOwnershipを持たなくなる

Team API

チーム間の良い相互作用を作るためにチームをAPIとして考える
チーム間の依頼のIFをしっかり定義して非同期的な動きができると良い
また、Team APIをしっかりと定義するとよい

テンプレートは下記

github.com

Fundamental Topologies

役割の曖昧な複数のタイプのチームがあると責任の所在がわからなくなる
「Team Topologies」が提唱してるのは下記4つのチームに制限すること

基本的にStream aligned teamが根幹で、それ以外の3つのチームがStream aligned teamの負荷を軽減する

Stream aligned team

  • ビジネスにおいて1番重要なチーム
  • ビジネスドメインに沿った開発を行う

Enabling team

  • 機能開発で時間がないチームに対して新技術やプラクティスの導入を支援していくチーム
  • 複数のStream aligned teamを横断的に支援
    • 適切なツール、プラクティスの調査、提案
    • 実作業でなくガイダンスの提供、短期的な支援
  • 永久にそのチームにいるわけではなく一時的な支援(自立支援)

Complicated sub-system team

  • 専門知識が必要な複雑なサブシステムを開発運用するチーム
    • たとえばクレジットカードのプロセッシング、画像や動画の配信部分、AIや機械学習など
  • 目的はStream aligned teamの認知負荷を下げること

Platform team

  • インフラ周りや共通基盤、ObservabilityやDeliveryを提供するチーム
  • これによりStream aligned teamの認知不可を軽減する
    • Developer Experienceを最重視
    • Stream aligned teamの邪魔にならないように
      • 最低限でシンプルなものにする
      • なんでもかんでも提供すればいいってもんでもない
        • 認知負荷の増大につながる

Team firstな境界

目指すべきは分離が容易な疎結合
チームの認知負荷に合わせてソフトウェア境界を選ぶ
素早いDeliveryを実現されるにはStream aligned teamが単一のドメインにたして責任をもつのが単純で手っ取り早い
疎結合にしていくにはモノリスがあることを認識することが重要だが、モノリスにも種類がある

モノリスの種類

  • アプリケーションモノリス
    • 複数の依存関係をもつ単一で巨大なアプリケーション
  • データベースモノリス
    • 同一DBのスキーマと結合している複数のアプリケーション
  • モノリシックビルド
    • 単一のCIでビルドを行う
    • コードベース全体でのビルド
  • モノリシックリリース
    • すべてのコンポーネントをまとめて同一環境に導入しなくてはいけないリリース
  • モノリシックモデル
    • 単一のドメインや表現を多くのコンテキストで強制する
  • モノリシック思考
    • 単一のスタックやツールを強制
    • Ownershipが保たれずモチベが低下する要因になる
  • モノリシックワークスペース
    • 1人ずつ隔離されたスペース

境界を見つける

節理面をさがす

  • ビジネスドメインのコンテキスト境界
    • 基本はこれ
  • 業界特有の規制に対応特化のチームの組成
    • PCI DSSとか
  • システムの変更頻度による分割
  • などなど

Team Interaction Mode

それぞれのチーム間のインタラクションは下記

  • Collaboration
    • 他のチームと一緒に働く
    • コラボにより摩擦は発生する
  • X-as-a-Service
    • コラボレーションを最小にしてツールやAPIを提供する
    • Stream aligned teamとPlatform team
  • Facilitation
    • 他のチームの補助
    • Stream aligned teamとEnabling team

さいごに

開発生産性を向上させるためにチーム構成を考えることも重要
ただ、組織に正解はないと思うので、自分達にあったものを取捨選択していくことが大事
そのためには先人の知恵を知っておくことも重要

参考情報

サイバーエージェントに入社しました

2023-09-01から株式会社サイバーエージェントのDeveloper Productivity室にDPE(Developer Productivity Engineer)として入社しました @uncle__ko です
気がつけば入社して1ヶ月が経ったこともあり、こうして入社エントリを書かせて頂こうかと思いました
このエントリではなぜサイバーエージェントに転職したのか、サイバーエージェントで何をやっていくのかを簡単に記載していこうかと思います

簡単な経歴

ざっくりですが自分の経歴を振り返っていこうかと思います

SIer時代

銀行の帳票システムや勘定系、市場系のシステムを見たりしてました
z/OSやFACOMなどのメインフレームの知識、COBOLやJCLの知識を取得しました(今後の人生の役に立つのかはわかりません)
その後、クレジットカードの基幹系システムを作ったり、Gatewayを作ったり、信用情報照会機関(CIC、JICCなど)と接続してスコアリングシステムの構築なども行いました
金融系の経験が多かったかと思います
そんな中、電子書籍を販売するサイトのBackend Engineerとして、はじめてWeb系の会社に常駐する経験をしました
いままでの金融系の現場との違いを肌で感じて、自分もWeb系の企業で働きたいと思うようになりました

ECサイト時代

運良くECサイトを作っているWeb系の企業に転職し、Backend EngineerとしてJoinしました
AWSを使用し、言語はPHPな環境で色々な経験ができました
そもそも専任のインフラチームがなかったのでインフラ領域を触ることもできましたし、一時期はBackend Engineerが僕だけになってしまった時期もあり、採用に関わったりビジネス職の人たちとのMTGにも沢山呼ばれたりして、色々な知識をつけることができました
ECサイトが好調に業績を伸ばして、負荷もかなり増大する経験をして負荷試験なども行うようになりました

アドテク時代

ハイトラフィックな環境に興味が出てきたのでアドテク業界にいきました
DSP/DMPを主に見ることになり、秒間40万リクエストを50ms以内に返すためのシステムの設計/開発というあまり経験できない経験ができました
ここのエンジニアたちはみな優秀でいい刺激をもらいましたし、ハイトラフィックが故の考慮事項やオンプレな環境の経験も積めました

FinTech時代

比較的大きい企業の経験が多かったのでスタートアップ企業で働いてみたいと思っていたのと、その当時話題になっていたマイクロサービスやGo言語を使った開発がしたいとおもい、FinTechのスタートアップ企業に入社しました
お金を扱うが故の苦労やマイクロサービスが故の大変さを経験しました
そんな中、以前から興味のあった開発生産性向上を目的としたチームを立ち上げたりしました

Tech Teamの生産力を爆上げすべくGrowth Technology Teamが爆誕しました - Kyash Product Blog

なぜサイバーエージェントに転職したのか

FinTechのベンチャー企業時代にチームの立ち上げを行っているように、開発生産性の向上には個人的にとても興味がありました
FinTechベンチャー企業時代にはFourKeys基盤の構築やFeature Flagの導入、Staging環境の構築などを行ってました
FourKeys基盤の構築に関しては下記登壇資料をぜひ御覧ください

speakerdeck.com

そして、サイバーエージェントにはDeveloper Productivity室という開発生産性の向上を専門に行っている組織があります
自分が長年エンジニアをやってきて、エンジニアがより生産性を発揮して輝ける世界に少しでも貢献できるといいなという思いもあります
そんな開発生産性の向上をミッションにしている組織からのお誘いもあり、自分の興味の方向性ともフィットしてしていると感じたので、この度サイバーエージェントにJoinさせていただきました

site.developerproductivity.dev

サイバーエージェントで何をするのか

ざっくり言うと、サイバーエージェントグループにおける事業開発の開発生産性を高める動きをしていきます
直近だと2023年版のState of DevOps Reportのまとめを書いたりしました

site.developerproductivity.dev

そんなDeveloper Productivity室には現在2つのプロダクトがあります

ひとつはPipeCDというOSSとして開発されている継続的デリバリーシステムです
KubernetesやECSといった様々なアプリケーションにおいて、統一的なGitOpsスタイルでのプログレッシブ・デリバリーを実現するプロダクトです 2023年5月にCNCF Sandboxプロジェクトに採択されたりしてます

pipecd.dev

もうひとつはBucketeerというOSSとして開発されているフィーチャーフラグ・A/Bテストシステムです
トランクベース開発やデータドリブンでの意思決定を支えるための基幹プロダクトです

bucketeer.io

僕はと言うと、開発生産性 x 生成AIでなにか新しいプロダクトを作れないかを思案したりしてます
いろいろ考える中で、雑にPoCを作ったりもしました

unless.hatenablog.jp

社内基盤・OSS問わず、会社全体の開発生産性向上を高めるための新しいプロダクト創造をしていく所存ですし、開発生産性に真摯に向き合う日々を送ろうと思います

さいごに

規模の大きい組織の中で、横軸で開発生産性を向上させるというミッションは、ベンチャー企業とはまた別の大変さがあるように思います
規模が大きすぎて、何から手をつければ良いのか、どのように浸透させていくべきか、何やら雲を掴むような感覚を日々感じてます
ですがありがたい事に、チャレンジへの失敗に寛容な組織であるようなので、せっかくならでかいことやって派手に成功するか爆散するようなチャレンジをするのも悪くないかもしれないです

そんなDeveloper Productivity室ではまだまだ一緒に働く仲間を募集中です
開発生産性の向上に興味がある人やDevOpsに興味がある人はぜひ一度カジュアル面談しましょう!

site.developerproductivity.dev

今更ながらおうちKubernatesクラスター作ってみた

はじめに

k8sもいろいろなシステムで採用されるようになった昨今、仕事でk8sを触ることも増えてきた
概要などは把握しているが仕事で触ることも増えてきたので、k8sの勉強を本格的にしようかと思ったりした
資格を取るのもありだが、自分で触ってみるのが一番速いという経験のもと、ひと昔前に流行ったおうちKubernatesクラスターを作ってみることにした

おうちKubernatesクラスターを作ろう(物理編)

今回はせっかくなのでRaspberry Piを使って物理てきに構築した VM上で構築しなかった理由は、物理端末があったほうがテンションがあがるじゃん?

パーツたち

Kubernatesクラスターを作るのに用意した素材は下記

商品 個数 値段 備考
Raspberry Pi4 ModelB 4GB 2 9,660 Amazonプライムデーで買った
Raspberry Pi4クラスターケース 1 1,990 冷却ファンとかついてるし安いしかっこいいよね
TP-Link TL-SG105E スイッチ 1 2,350 プライムデーで安かったので
BUFFALO microSDXC 64GB 2 700 とりま64GBあれば足りるかなって思って
Micro HDMI to HDMI 変換ケーブル 1 890 これが必要なのがだるいとこ
LANケーブル 4 priceless 家とかに余ってるよね。短いの3本、長いの1本で足りた

最初は3台構成にしようかなって思ったけどラズパイ本体が高くて2台で妥協した

パーツたち

Kubernatesクラスターの構築(物理)

物理構築はそんなに難しいものではない クラスターケースに書いてる組み立て方をみて、あとは雰囲気で作っていけばそれらしいものが1,2時間ほどで完成する
ガンプラのHGのほうが難しいので小学生でも作れるはず

Raspberry Pi4は熱を発しやすいので発熱対策はしたほうがよさそう

一段目

二段目

そんなこんなで完成
やっぱり物理端末があるほうがかっこいいし所有欲を満たしてくれるからいいよね

完成

おうちKubernatesクラスターを作ろう(論理編)

kubeadm

勉強のためだし1から構築するのもアリだが、サクッと作って動かした方が楽しいかなって思ったので今回はkubeadmを使って構築していく

github.com

CRI

コンテナランタイムはcontainerdを使用する

CNI

ネットワーク部分には無難にCalicoを使用する

github.com

OSのインストール

Raspberry Pi Imagerというツールが公式から出ているので、指示に従っていけばOSのインストールは簡単にできる

Raspberry Pi OS – Raspberry Pi

なんなら動画もあるのでこれ見れば一発でわかる

youtu.be

今回はUbuntu 23.04(64-bit) を使った
理由は使い慣れてるから

hostnameの設定

今回は2台なのでどちらでも設定する

hostnamectl set-hostname k8s1
hostnamectl set-hostname k8s2

IPの固定

/etc/hosts に払い出されたIPを設定

10.0.0.11  k8s1
10.0.0.12  k8s2

kubeadm, kubelet and kubectlのインストール

公式の手順に準ずる
ドキュメントにも書いてあるけど、swapを無効化するのを忘れないように

kubernetes.io

kubeadmでk8sクラスターの初期化

$ kubeadm init --pod-network-cidr=10.244.0.0/16 --control-plane-endpoint=k8s1 --apiserver-cert-extra-sans=k8s1

基本的にkubeadm initを打てばその後に実行すべきコマンドの指示があるのでそれに従えばよい

kubectlコマンドが使用する認証情報をコピーする

$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

Worker NodeのJoin

Worker Nodeでクラスタへのjoinコマンドを実行
kubeadm initで表示されるトークンを使ってjoinする

$ kubeadm join k8s1:6443 --token xxxxxxxx \
    --discovery-token-ca-cert-hash sha256:yyyyyyyyyyyyyy

CNIのインストール

ドキュメントに準ずる   これにより各ノードで実行されるPod同士が通信できるようになる

docs.tigera.io

Kubernetesクラスタの状態確認

下記などでクラスタの状態を確認する

$ kubectl get nodes
$ kubectl cluster-info

さいごに

数年前に流行ったおうちk8sクラスターをいまさらながら作ってみた
最近になってk8sを仕事で使う機会も増えてきたのもあり勉強ついでに作ってみたりしたが、やはり手を動かしながらのほうが覚えられるし理解が進むなと改めて思った
クラスタ作って力尽きたので、今度はPrometheusとGrafanaで定番のダッシュボードでも作ろうかなと思ってる

image

というわけで、みなさんもたのしいKubernates Lifeを!

OpenAI APIを使ってサクッと生産性を上げるツールを作ってみた

みなさんもご存知のように、ChatGPTによる台頭が生成AIブームを引き起こしています
せっかくなのでこの波に乗ってみようと思い、OpenAIのAPIを使用して簡単なツールを作成しましたので紹介します

OpenAI API

OpenAI APIは、ChatGPTで有名なOpenAIが提供しているAPIです

openai.com

OpenAIのAPIサービスにはいくつかの種類があります。どのような種類のAPIがあるかはDocsを参照することでわかります

詳しい解説は別のブログに譲ることにします

commit-wiz

github.com

1つ目は"commit-wiz"というツールを作成しました
このツールはGitのDiffを読み取り、どのような変更内容かを解析し、Commitコメントを提案してくれるツールです
きちんとした組織であればCommitコメントが整備されている環境もあるかと思いますが、ベンチャー企業などではCommitコメントの粒度やフォーマットが整っていないこともよくあります

さらに、"Fix"や"commit"といったコメントが横行することもよくあるかと思います
このツールを使用することで、簡単にマシなCommitコメントを生成することができます
ちなみに、このツールのCommitコメントのほとんどはこのツール自体で生成されています

Commits · ouchi2501/commit-wiz · GitHub

Commitコメントの生成にはChat completions APIというAPIを使用しています

platform.openai.com

func requestOpenAI(ctx context.Context, key string, gitDiff string, length int) string {
    client := openai.NewClient(key)
    response, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
        Model: openai.GPT3Dot5Turbo,
        Messages: []openai.ChatCompletionMessage{
            {
                Role:    openai.ChatMessageRoleSystem,
                Content: fmt.Sprintf(systemContent, length),
            },
            {
                Role:    openai.ChatMessageRoleUser,
                Content: gitDiff,
            },
        },
    })
    if err != nil {
        panic(err)
    }
    return response.Choices[0].Message.Content
}

このツールはGo言語で作成しており、公式サイトに掲載されている有志によるコミュニティライブラリを利用しています

OpenAI Platform

このライブラリを使用することで、OpenAIのAPIをかなり簡単に利用できます
実際、このツール自体もコンパクトな実装で済んでおり、大枠を作成するのに1時間程度しかかかりませんでした

code-explain

github.com

image

2つ目は、選択したコードを英語または日本語で要約してくれるNeovimのプラグインを作成しました
自分自身の経験から、エンジニアはコードを書く時間よりも読む時間の方が多いと考えています
また、読み取りにくいロジックに出くわすことも多いです
そこで、このプラグインを使用しAIがコードの処理内容を要約してくれることで、コードの理解を迅速化できるでしょう
VS CodeにはGitHub Copilot Labsによって同様の機能が提供されていますが、自分はNeovimを使用しているためNeovim向けのプラグインを作成しました

githubnext.com

こちらもcommit-wizと同様にChat completions APIを使用しており、curlを使って呼び出しています
Vimプラグインの作成は初めての経験でしたが、意外と簡単に作成できました

さいごに

このように、最近話題の生成AIを活用して簡単なツールを作成しました
OpenAIのAPIは様々な言語のコミュニティライブラリが充実しており、使いやすいため生成AIを活用したツールやプロダクトのアイデアがある方はぜひ挑戦してみることをお勧めします

また、私が所属しているCyberAgentのDeveloper Productivity室ではDeveloper Productivity Engineerを募集してます
開発生産性を高めるための特命ミッションを担うチームで、技術者への最高の開発体験実現と、技術者が本質的な開発に専念できるようにするための環境づくりをしています
開発生産性の向上やDevOpsに興味があれば、ぜひカジュアル面談しましょう

site.developerproductivity.dev

株式会社Kyashを退職しました

3年間お世話になった株式会社Kyashを2023/02/28付けで退職することになりました
一緒に働いていた皆さん、大変お世話になりました
ありがとうございました

この3年間たくさんの経験をさせてもらったので、やってきたことをまとめようかと思います

Kyashでやってきたこと

入社初期時代

AML(Anti Money Laundering)周りのシステムの設計/開発

詳細は書けませんが、AML用の検知をするためのシステム設計や開発をやらせていただきました
AMLとは悪い人が資金洗浄できないようにするための対策のことです
Anti Fraudチームの方と協力して怪しい動きをしているユーザを見つけ出せるようにしました

負荷対策

入社間もなくKyashカードのリリースだったのですが、高負荷になりアプリが落ちてしまう状態になってしまいました
このようなことが起きないように、さまざまな負荷対策を実施しました
僕が実施した負荷対策の中で大きいものを書くと、ユーザのSession管理をPostgreSQLからRedisへの移行を行いました
高負荷状態のときのボトルネックPostgreSQLになっていたので、Readの件数を減らしかった背景があります
またSessionへのアクセス頻度はかなり高く保持すべき情報も難しいrelationが必要なわけでもないので、RDBではなくIn-MemoryなKVSであるRedisを選択しました

さらに負荷試験環境の構築や、定期的に負荷試験を実施するための体制作りなども行いました
こちらについては過去に記事を書いているので、興味がある方はそちらをご覧ください

unless.hatenablog.jp

unless.hatenablog.jp

Funds Team時代

社内での体制変更があり、Micro Serviceごとにドメイン領域に特化したチームを立ち上げることになりました
自分はお金の入出金周り(Funds-in/Funds-out)と残高周り(Account)を担当するFundsチームの立ち上げから参画することになりました

Kyashには沢山の入出金手段があるので、この周辺のシステムを担当していました

選べる入金方法 - Kyash

現金が必要なときに。残高の出金 - Kyash

銀行口座入金

各銀行とシステム接続をして、銀行口座から直接Kyashに入金できるようにするシステムの設計/開発を行いました
Fundsチームとしての最初の仕事である銀行口座入金はレガシーなシステムとの接続ということもあり、非機能要件に苦労したり電文の形式が特殊なことが多くParserを自前で実装しなければいけない苦労や暗号化周りでの苦労など、FinTechならではの沢山の苦労と経験をさせてもらえました
個人的にはこの辺の泥臭い作業が大好きだったので楽しく仕事が出来たのと、あまり他では経験できないような事が多くとても勉強になりました

ATM出金

セブン銀行ATMとシステム接続して、Kyashの残高を現金としてATMから出金できる機能を作りました 接続試験のために丸の内のセブン銀行本社まで行ったのはいい思い出になりました
リリース時もセブン銀行本社に夜中に出向いたのを覚えています

この機能は個人的にすごく便利だと思っている機能だったので、リリースできた時の喜びもひとしおでした

BNPL

Kyashにある後払いサービスであるイマすぐ入金の設計/開発を行いました
このタイミングで、社内のサービスにgRPCを導入しようという話になり、Fundsチーム主導で初のgRPCを使ったMicro Serviceが爆誕したりしました

イマすぐ入金 - コンビニで後払いできるアプリ/Visaカード - Kyash

Bravo Team時代

会社の組織変更のタイミングがあり、人数が減った関係でドメイン特化型のチームからAlphaとBravoの2チーム体制になりました
このタイミングで僕はBravo側に配属されました
Bravoは基本的にはPayment(決済)システム周りをメインに見るチームでした

Lite&Virtual Cardの有効期限対応

Lite&Virtual Cardの有効期限が切れるということで、カードの再発行やPaymentシステム周りやQUICPay+の連携周り、カード発行システム周りなどでカードの有効期限が切れた時の対応をしました

Visaとのテスト環境の一新

もともとあったVisaとの接続テスト用環境が社内のWindows PCに存在していたので、EC2への移行と関連するMicro Serviceの再構築を行いました
これでVisaとのテストがしやすくなりました

Growth Technology Team時代

開発生産性の向上のためのチームを立ち上げました
詳しくはこちらで記事にしてますので、興味があればご覧ください

blog.kyash.co

Fourkeys基盤の構築

開発生産性の向上にあたり、そもそもの現状を把握できなければ改善する事は出来ないので、現状を可視化するための基盤を作成しました
こちらは登壇資料がありますので、興味があるかたはぜひご覧ください

unless.hatenablog.jp

Staging環境の整備

Kyashではインフラ含めて本番に近い擬似本番環境というものが存在していませんでした
また、QAチームで利用している環境と開発者が普段開発している環境が同じであったりして、安全で健全な状態とは言い難い環境でした
この問題を解決するために擬似本番環境の構築を実施していました
この対応は最後まで成し遂げたかったのですが、有休消化前に終えることが出来ず、チームメンバーに託す形となりました
完成した暁には、Kyashのmeetupやブログ等で記事が公開されると思っているので楽しみに待っています

その他

ここには書き切れない細かいことや社外秘にあたるので公表できないことなど、かなり多くの経験をさせていただきました
Profilerの導入もさせてもらいましたし、チームでLayered Architectureの導入やDBの自動マイグレーションの仕組みの構築などもさせていただきました

なんで退職するの?

自分はあまり英語が得意ではないのがずっとコンプレックスでした
エンジニアとしてのキャリアを考えた時に、英語を使って仕事が出来たほうが選択肢の幅が広がるのでいつか勉強せねばと思っていました
もちろんOSSにissueをあげたりDocumentを読む程度の英語力はありますが、時間もかかるし正しく伝えられているのかの自信は今でもないです
そしていっそのこと、英語が公用語ないしは公用語にしようとしてる日本企業に転職しようと思い、このたび転職することにしました

つぎは何をするの?

今はまだ有給消化中なので英語の勉強をしたり、趣味でCPUの勉強をしています
次の会社は公用語を英語にしようとしていて、拙い英語でも受け入れてくれる企業でした
試用期間が終わり、モチベがあれば入社エントリを書くかもしれません

さいごに

たくさんの経験をしたからか長いようで短い3年間でした
Kyashは意外と代替えのない便利なサービスだと思ってます
今後は1ユーザとして応援させていただければと思います
3年間ありがとうございました!

なぜGolangが一部のBinary Toolを自前で実装しているのか

これはなに?

これはKyash Advent Calendar 2022 21日目の記事です
KyashでBackend Engineerをしている @uncle__ko です
お金の入出金を司るチームや決済領域を司るチームなどを経験したあと、現在はTechチームの生産性向上に向けた取り組みを行うチームのリードを行っています
興味がある方はぜひ、下記の記事を御覧ください

blog.kyash.co

Golangは自前でobjdumpやnm、addr2lineなどのToolを内包している

さっそく本題に入りますが、Golangには objdumpnmaddr2line などのbinary解析でよく使用されるToolが内包されている珍しい言語です

これからのbinary解析でよく使用されるToolはGNU Binutilsなどで提供されているので、わざわざ自前で管理しておくメリットは薄く感じます

www.gnu.org

それなのに、なぜこのようなToolを内包させてるのかなとふと疑問に思ったので、考古学者になって歴史探索しようかなと思いました

GNU Binutilsなどで管理されているBinary Tool

Golangが独自で管理している3つのBinary Toolについて、そもそもGNU Binutilsでの挙動を軽くおさらいしておきます

objdump

見たままではあるのですが、これはobjfileの情報を表示するためのToolです
optionでどの情報を表示するかを制御します

sourceware.org

叩いてみるのがはやいと思うのでシンプルな Hello, World を出力するコードで試してみます

package main

import "fmt"

func main() {
  fmt.Println("Hello World!")
}
$ go build hello.go
$ objdump -h hello

hello:  file format mach-o 64-bit x86-64

Sections:
Idx Name             Size     VMA              Type
  0 __text           0008c1a7 0000000001001000 TEXT
  1 __symbol_stub1   00000114 000000000108d1c0 TEXT
  2 __rodata         0003a4d6 000000000108d2e0 DATA
  3 __typelink       00000548 00000000010c77c0 DATA
  4 __itablink       00000070 00000000010c7d20 DATA
  5 __gosymtab       00000000 00000000010c7d90 DATA
  6 __gopclntab      0005e170 00000000010c7da0 DATA
  7 __go_buildinfo   00000120 0000000001126000 DATA
  8 __nl_symbol_ptr  00000170 0000000001126120 DATA
  9 __noptrdata      00010640 00000000011262a0 DATA
 10 __data           000074f0 00000000011368e0 DATA
 11 __bss            0002f120 000000000113dde0 BSS
 12 __noptrbss       00004988 000000000116cf00 BSS
 13 __zdebug_abbrev  00000129 0000000001172000 DATA, DEBUG
 14 __zdebug_line    0001e087 0000000001172129 DATA, DEBUG
 15 __zdebug_frame   00005d40 00000000011901b0 DATA, DEBUG
 16 __debug_gdb_scri 00000046 0000000001195ef0 DATA, DEBUG
 17 __zdebug_info    0003745a 0000000001195f36 DATA, DEBUG
 18 __zdebug_loc     0001cc3f 00000000011cd390 DATA, DEBUG
 19 __zdebug_ranges  000084cb 00000000011e9fcf DATA, DEBUG

とりあえずわかりやすいようにsection headerを表示してみました
Macでbuildしたコードなのでfile formatが mach-o になってますね

実行環境は下記です

$ sw_vers
ProductName:    macOS
ProductVersion: 12.6
BuildVersion:   21G115
$ go version
go version go1.19.2 darwin/amd64

また、objdumpを使って逆アセンブルしたりもできます

$ objdump -d -j __text hello | head -10

hello:  file format mach-o 64-bit x86-64

Disassembly of section __TEXT,__text:

0000000001001000 <_runtime.text>:
 1001000: ff 20                         jmpq    *(%rax)
 1001002: 47 6f                         outsl   (%rsi), %dx
 1001004: 20 62 75                      andb    %ah, 117(%rdx)
 1001007: 69 6c 64 20 49 44 3a 20       imull   $540689481, 32(%rsp,%riz,2), %ebp ## imm = 0x203A4449

nm

nmはオブジェクトファイルに含まれているシンボルをリストアップするToolです

sourceware.org

先程のhelloに対して実行すると下記のような感じです

$ nm hello | head -10
00000000010c58d8 s _$f64.3eb0000000000000
00000000010c58e0 s _$f64.3f50624dd2f1a9fc
00000000010c58e8 s _$f64.3f847ae147ae147b
00000000010c58f0 s _$f64.3fd0000000000000
00000000010c58f8 s _$f64.3fd3333333333333
00000000010c5900 s _$f64.3fe0000000000000
00000000010c5908 s _$f64.3fe8000000000000
00000000010c5910 s _$f64.3ff0000000000000
00000000010c5918 s _$f64.3ff199999999999a
00000000010c5920 s _$f64.3ff3333333333333

addr2line

addr2lineはデバッグ情報を利用してファイル名と行番号の情報を取得するToolです

sourceware.org

こちらに関して、実はmacOSには入ってないです
興味がある人はLinux環境を用意して自分で叩いて見るのも面白いかもしれません
ぜひご自身で確認してみてください

Golangに内包されているBinary Tool

まずはGNU Binutilsと同じように動作するのか実際に叩いてみます

go tool objdump

$ go tool objdump -h hello
usage: go tool objdump [-S] [-gnu] [-s symregexp] binary [start end]

  -S    print Go code alongside assembly
  -gnu
        print GNU assembly next to Go assembly (where supported)
  -s string
        only dump symbols matching this regexp

optionがすべて許容されているわけではなさそうですね
必要最小限のものだけをGolangで作って管理しているのでしょう

与えられているoptionを指定して実行した結果も貼っておきます

$ go tool objdump -gnu hello | head -10
TEXT go.buildid(SB)
  :-1           0x1001000       ff20            JMP 0(AX)                            // jmpq *(%rax)
  cpu_x86.s:4       0x1001002       476f            OUTSD DS:0(SI), DX                   // rex.RXB outsl %ds:(%rsi),(%dx)
  cpu_x86.s:4       0x1001004       206275          ANDB AH, 0x75(DX)                    // and %ah,0x75(%rdx)
  cpu_x86.s:4       0x1001007       696c642049443a20    IMULL $0x203a4449, 0x20(SP), BP      // imul $0x203a4449,0x20(%rsp,%riz,2),%ebp
  cpu_x86.s:4       0x100100f       226f36          ANDB 0x36(DI), CH                    // and 0x36(%rdi),%ch
  cpu_x86.s:4       0x1001012       6e          OUTSB DS:0(SI), DX                   // outsb %ds:(%rsi),(%dx)
  cpu_x86.s:4       0x1001013       51          PUSHQ CX                             // push %rcx
  cpu_x86.s:4       0x1001014       56          PUSHQ SI                             // push %rsi
  cpu_x86.s:4       0x1001015       33482d          XORL 0x2d(AX), CX                    // xor 0x2d(%rax),%ecx

go tool nm

$ go tool nm -h
usage: go tool nm [options] file...
  -n
      an alias for -sort address (numeric),
      for compatibility with other nm commands
  -size
      print symbol size in decimal between address and type
  -sort {address,name,none,size}
      sort output in the given order (default name)
      size orders from largest to smallest
  -type
      print symbol type after name

こちらも必要最小限のoptionが用意されているだけに見えます

実行した結果は下記です

$ go tool nm hello | head -10
 10c58d8 R $f64.3eb0000000000000
 10c58e0 R $f64.3f50624dd2f1a9fc
 10c58e8 R $f64.3f847ae147ae147b
 10c58f0 R $f64.3fd0000000000000
 10c58f8 R $f64.3fd3333333333333
 10c5900 R $f64.3fe0000000000000
 10c5908 R $f64.3fe8000000000000
 10c5910 R $f64.3ff0000000000000
 10c5918 R $f64.3ff199999999999a
 10c5920 R $f64.3ff3333333333333

go tool addr2line

$ go tool addr2line -h
usage: addr2line binary
reads addresses from standard input and writes two lines for each:
    function name
    file:line

こちらも必要最小限の機能です

objdumpの探索

ひと通りの振り返りも済んだので、まずはobjdumpの考古学をしていきます
とりあえず、現在のファイルはいつ作られたのかを追ってみます

実行ファイルはここです
このファイルの変更履歴を辿ってみます

何やら、このcommitが最初のようです

github.com

commit commentがしっかり記載されていますね

Update cmd/dist not to build the C version. Update cmd/go to install the Go version to the tool directory.

と記載があるので、Cで書いていたコードをGolangに置き換えたことが読み取れます

Update #7452

と記載があるので、そのissueも見てみる必要がありそうです

github.com

This is the basic logic needed for objdump, and it works well enough to support the pprof list and weblist commands.

上記を読むと、なにやらpprof list周りで何かがあったのかもしれません
もしかしたらissueに記載があるかもしれないので、そちらを辿ってみます

On darwin, running the list command in pprof produces an error from the go-supplied objdump. Weblist is similarly affected. What do you see instead? objdump: syminit: Undefined error: 0

issueを見ると、darwin環境だとpprofでprofilingするとエラーになっていたようですね

Go 1.2.1 (homebrew) works, tip does not. I believe it has something to do with one of the changes to address https://golang.org/issue/6853 and objdump hasn't been brought up-to date.

と記載があるので、別のissueの対応のせいでぶっ壊れたと推測していることがわかります

Russ氏も

This is a known problem; I need to rewrite objdump in Go to make it work again

ここでコメントしているので、このタイミングでGolangに書き直す必要があったのだと思われます
Golangに書き直す理由の話なので、今回はあまり関係はなさそうなので、一旦Cのコードがあったときのコードを辿っていくことにしましょう

ちなみに、この原因になっているissueもかなり面白い問題ではあり、興味がある人は追いかけてみてもいいかもしれません
このissueはいまだに閉じていない問題で、GolangのVersionが上がるたびにどんどんbinary sizeが大きくなって来ているので、なんとかしたいという問題です
興味がある人は下記を辿ってみてください

github.com

気を取り直して、Cの実装があった頃を辿ります
C言語のファイルが出来たのは、このタイミングのようです

github.com

こちらもcommit commentがしっかり記載されています
考古学をしていると、commit commentの大切さがよくわかります
自分自身もcommit commentをしっかり書こうと思わされますね

runtime/pprof: support OS X CPU profiling Work around profiling kernel bug with signal masks. Still broken on 64-bit Snow Leopard kernel, but I think we can ignore that one and let people upgrade to Lion.

OS Xのbugのせいで困ってるようです

Add new trivial tools addr2line and objdump to take the place of the GNU tools of the same name, since those are not installed on OS X.

なんと、OS Xには addr2lineobjdump が入っていないから、自前でtrivalなtoolsを作ったようですね
意図せず addr2line の謎も解けそうです

Adapt pprof to invoke 'go tool addr2line' and 'go tool objdump' if the system tools do not exist.

system toolsにこの2つが入っていないときに go tool を使うようにしたみたいですね

OS Xのbugについては、こちらのIssueに記載されています

github.com

メールでも言及されている通り、OS Xはvirtual cpu timer signalをCPUを使い切ったThreadにちゃんと送らないのが問題のようですね

OS X doesn't reliably deliver the virtual cpu timer signal to the thread that used up the cpu

Go Profiling - Symbols not available

結論

pprofが addr2lineobjdump を使用するため、 この2つのToolがinstallされていない環境でも動くように、自前でpprofが動く最小限の機能だけを自前で実装することにした
ということがわかりました
Golangはbinaryのdeliverをかなり重視している気がしていて、single binaryで動作出来るようにしているし、様々な環境でも動くように自分たちで腹くくってエコシステム周りを自前で実装してる印象があります

addr2lineの探索

objdumpの探索で一緒に解決されたので、そちらを参照ください

nmの探索

nmについても、同じようにhistoryを辿っていきましょう
ここを見る限りは、これが一番古いcommitのようです

github.com

cmd/nm: reimplement in Go

ここについてもCで書かれていたものをGolangで書き直していそうですね

The immediate goal is to support the new object file format, which libmach (nm's support library) does not understand. Rather than add code to libmach or reengineer liblink to support this new use, just write it in Go. The C version of nm reads the Plan 9 symbol table stored in Go binaries, now otherwise unused. This reimplementation uses the standard symbol table for the corresponding file format instead, bringing us one step closer to removing the Plan 9 symbol table from Go binaries.

ここに書いてある通り、libmatchが理解出来ない新しいobject fileのフォーマットサポートするために、libmatchをメンテするんじゃなく、Golangで書けばいいじゃないってことです
また、読み取るsymbol tableをstandardにすることで、GolangのbinaryからPlan 9のsymbol tableを削除するのに1歩近づいたそうです

とはいえ、Cの実装を持ち込んだのかはわからないので、historyを辿ってみます

github.com

これが最初のcommitですが、あまり情報がありませんね...
2008/8/4とかなり古いので、正式リリース前です

とはいえ、その後のcommitを見る限りはGolang特有のsymbol tableを読み取らせるためのような気もしますね

結論

ちゃんとしたことは分からないけど、おそらくGolang特有のsymbol tableを読み取らせたかったのだろうと思いました
Golangはかなりアセンブラも特殊なので、開発中に自前のnmあるほうが何かと便利だったのだろうという推測は出来きました
僕はここまでしか追いきれなかったので、ここで議論されてるなどの情報があればぜひ教えてください

最後に

今回はふとした疑問から、なぜBinary Toolの一部を自前で実装しているのかを調べてみました
Golangはかなりマルチプラットフォームを意識しているんだなと改めて実感しました

僕が考古学をするときはこのような形で過去のcommitや議論を追ったりしてます
他の方の参考になれば幸いですし、こんなやり方してますっていう他のアプローチがあればぜひ教えてほしいです

また、Kyashでは絶賛採用強化中です
Golangが好きなEngineerもこれからGolangを使って働いてみたいEngineerも大歓迎ですので、興味があればぜひ応募してみてください

株式会社Kyash の全ての求人一覧