Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPからGoへのマイグレーション for DMMアフィリエイト

PHPからGoへのマイグレーション for DMMアフィリエイト

DMM.go #9
DMMのアフィリエイト基盤をPHPからGoにマイグレーションした際の取り組みや、PHPerがGo開発で気をつけることについて

yabako kobayashi

December 12, 2024
Tweet

Other Decks in Technology

Transcript

  1. © DMM.com 既存のアーキテクチャ PHPフレームワークを使用したシンプルな構成 • 複数の画面 ◦ アフィリエイターが閲覧する画面 ◦ 管理者が操作する画面

    • 定期実行されるバッチ処理 • クリック/購入を登録するインターフェース • etc… これらが同じリポジトリに詰め込まれたモノリシックな構成 6
  2. © DMM.com リプレイス後アーキテクチャ 工数や影響を考慮して段階的なリプレイスを採用 • ユーザーの画面を対象 ◦ 全ての画面を一気にリプレイスせず、変更後の 影響範囲を狭める •

    フロントとバックエンドは分離 ◦ 既存は密結合している ◦ 規模や報酬を扱うことを鑑みると、分離してス ケーラビリティや変更影響を限定的にしたい • DBはオンプレミスをそのまま使用 ◦ 報酬(金銭)の取引があるため個人情報が保存 されており、クラウド移行に大きな課題がある 8
  3. © DMM.com Go採用のモチベーション ❏ 部署内で言語統一の流れがあった ❏ Go言語への統一プロジェクトで進化を遂げる、マーケティングテクノロジー部の決意 移管されてきたプロダクト 各プロダクトごとに開発言語や環境が異なり、 相互に連携してるので複雑度が高い

    部署の課題 古いシステムが多い 属人化が激しく、メンテナンス性も著しく損 なっているので保守工数が増大 Goへの言語統一 • システム全体の一貫性の向上 • エンジニアリソースを流動的に配 置 生産性/メンテ ナンス性UP 課題へのアプローチ 9
  4. © DMM.com アフィリエイトに対するGoの合致度 1. パフォーマンス観点 a. データ量 i. いままで発生した報酬履歴 1.

    レポーティングなどで使用 ii. 未来に支払いする報酬の計算 b. 負荷 i. 動画などキャンペーン時にはアフィリエイトを通した大量リクエスト 1. 大体300rpsぐらい 2. スケーラビリティ(並行処理) a. リアルタイムデータの集計 b. レポートの生成 11 Goのイメージ ❏ パフォーマンスが高い ❏ コンパイルされたバイナリが直接実行されるため、レイテンシが格段に少ない ❏ メモリ管理が効率的 ❏ スケーラビリティに優位性がある ❏ 軽量なゴルーチン(並行処理)が簡単に使える ❏ シンプルな構造と明確な依存管理 アフィリエイト特性に当てはめて検討 Goにマッチしてそう
  5. © DMM.com Go開発の取り組み(プロジェクト構成) 13 課題1. 記述量増加 // Item構造体を定義 type Item

    struct { ID int `json:"id"` Name string `json:"name"` Price float64 `json:"price"` } // 指定IDのアイテムを取得する関数 func getItem(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") var item Item err := db.QueryRow("SELECT id, name, price FROM items WHERE id = $1", id).Scan(&item.ID, &item.Name, &item.Price) if err != nil { http.Error(w, "Item not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(item) } // 指定IDのアイテムを取得する関数 function getItem($id) { global $pdo; $stmt = $pdo->prepare("SELECT id, name, price FROM items WHERE id = ?"); $stmt->execute([$id]); return $stmt->fetch(PDO::FETCH_ASSOC); } // APIの呼び出し処理 header('Content-Type: application/json'); echo json_encode(getItem($_GET['id'])); Go PHP
  6. © DMM.com Go開発の取り組み(プロジェクト構成) 15 1. フロントとバックエンドのインターフェース共通化 ❏ OpenAPI定義を元にコードを生成 ❏ 使用ツール

    ❏ Backend: oapi-codegen ❏ Frontend: swagger-typescript-api ❏ OpenAPI定義とRes/Req構造体を見比べる必 要がなくなった ❏ レビュー負荷軽減 各リポジトリにサブモジュールとして配置
  7. © DMM.com Go開発の取り組み(プロジェクト構成) 16 2. DB操作はSQLによったものを選定 ❏ SQLから生成できるsqlc ❏ SQLクエリの書き方さえ知ってればよいので

    ORMのメソッドを調べたり、メソッドで表現できない複雑 なクエリとで書き分ける必要がない ❏ SQLはメンバー全員慣れていたので好都合 →しかしテーブル依存で断念。。。 ❏ 主な敗因 ❏ さまざまなシナリオをカバーするために複数の類似クエリを作成することが多い ❏ 構造体がNULLになりえるのにNOT NULL型で生成される(#1208) CREATE TABLE account ( id INTEGER PRIMARY KEY, name VARCHAR(128) NOT NULL, email TEXT ); SELECT email, (SELECT a.id FROM account a WHERE a.name = 'foo' LIMIT 1) AS "id" FROM account; type TestRow struct { Email sql.NullString ID int32 // 本当は sql.NullInt32 になってほし い! } 例:
  8. © DMM.com Go開発の取り組み(プロジェクト構成) 17 2. DB操作はSQLによったものを選定 (2回目) ❏ SQLファーストなORMを採用 ❏

    bun ❏ クエリビルダーの感覚で使用できる ❏ 機能が少ないので、なにを使うかで迷うことがない デメリット ❏ インデックスヒントやオプティマイザヒントは提供されてないので、必要なプロダクトの場合相性が 良くない ❏ 有名なJSランタイムの名前と被っているのでググラビリティがすこぶる悪い
  9. © DMM.com Go開発の取り組み(プロジェクト構成) 18 自動生成するなら goaという選択肢もあるのでは? ❏ まずgoaを採用していても大きな問題になるような構成ではなかった ❏ 今回に関しては既存の知識で解決できたので、追加の学習の観点で採用しなかった

    ❏ goaのDSLについて知る必要がある ❏ OpenAPI定義とSQLは広く知られている ❏ goa genにカスタマイズするみたいなパターンが出てきた時や、トラブルシュートに不安があった ❏ DMMではgoaは広く採用されているが、部署内では採用事例がなかった
  10. © DMM.com Go開発の取り組み(プロジェクト構成) 19 課題2. レイヤー分割 ❏ クリーンアーキテクチャを採用 ❏ Goのフレームワークには

    LaravelやCakePHP のようなフルスタックフレームワークのお決まり が無いことが度々ある ❏ 今後は規模が大きくなる想定だったので、責務 はある程度分割したかった ❏ ベースとなるルールを用意したかった ├── app/ │ ├── domain/ │ │ ├── model/ │ │ │ └── account.go │ │ ├── repository/ │ │ │ └── account.go │ │ └── service/ │ │ └── account.go │ ├── infra/ │ │ ├── dao/ │ │ │ └── account.go │ │ └── external/ │ │ └── db │ ├── interface/ │ │ └── account.go │ └── usecase/ │ └── account.go └── cmd/ └── main.go
  11. © DMM.com Go開発の取り組み(プロジェクト構成) 20 type Account struct { ID uint

    Email string Name string Created time.Time Modified time.Time } 最初はdomain/modelとテーブルが1対1で作成していた ❏ 開発初期は使用テーブルも少な かったので、不都合が発生しな かった ❏ 既存フレームワークもmodelが テーブル同等だったので、なんら 問題がないように思えた 課題2. レイヤー分割
  12. © DMM.com Go開発の取り組み(プロジェクト構成) 22 課題2. テーブル構造の負債 ❏ Domain Modelとテーブル操作用の structを分割

    ❏ Domain Modelとテーブル操作後の変換層を追加 ├── domain/ │ ├── model/ │ │ ├── account.go │ │ └── reward.go │ └── repository/ │ ├── account.go │ └── reward.go └── infra/ ├── adapter/ │ ├── account.go │ └── reward.go └── persistence/ └── entity/ ├── account.go ├── reward.go ├── price.go └── conversion.go repository adapter entity model dao use implement
  13. © DMM.com PHP開発者がハマるGoの開発 25 レシーバー /メソッド ❏ DIと合わせて「どうなってるか分かりづらい」と言われることあり ❏ 「クラスを定義する」という発想を捨て、「構造体に機能を持たせる」という考え方に切り替える方が理

    解してもらいやすい コンストラクタが独立しているかつ関数 名が決まってない PHP構文との違いだがclass(struct)の 中で宣言されていないので、メソッドな のが直感的でない
  14. © DMM.com PHP開発者がハマるGoの開発 26 グローバル変数 ❏ PHPではグローバル変数をリクエスト毎の共有として使ってしまったりする ❏ 例えば、セッション情報をグローバル変数に格納して、どの処理でもアクセス可能にする //

    PHPでのグローバル変数使用例 $_SESSION['user'] = $user; ❏ Goでグローバル変数を使うと... ❏ スレッドセーフではないため、データが競合し、アプリケーションが不安定になることがある ❏ 場合によってはOOMが発生する ❏ Goではcontextでリクエスト毎の情報伝搬を行なう
  15. © DMM.com コンテナサービスとの親和性 28 イメージサイズが小さい PHP(laravel) 578MB Go(echo) 15.3MB ❏

    イメージサイズが小さいのでpullが早くなる ❏ レジストリ管理コストが微量だけど削減 ❏ ストレージ料金
  16. © DMM.com コンテナサービスとの親和性 30 スピンアップが早い ❏ PHPにも動的コンパイルをキャッシュする仕組みがあり、これを利用することで実行速度を向上 できる 1. PHPスクリプトを読み込む

    2. パースして中間コードに変換 3. 中間コードを実行 4. 中間コードをキャッシュ保存 5. 次回以降、キャッシュした中間コード を再利用 ; OPcacheを有効化 opcache.enable=1 ; CLIスクリプトでも有効化(任意) opcache.enable_cli=1 ; キャッシュサイズを設定(単位: MB) opcache.memory_consumption=128 ; キャッシュするスクリプトの最大数 opcache.max_accelerated_files=10000 ; キャッシュが更新されるタイミング opcache.revalidate_freq=2 PHP実行の流れとopcache opcacheの設定
  17. © DMM.com まとめ 32 ❏ 開発中は紆余曲折あったけどなんとかリリースまで出来た ❏ ここをコストと思うか投資と思うかで、辛みが違う ❏ PHPからGoへの移行の注意点は、開発初期に集約されているように思える

    ❏ 開発チームへのGo言語の浸透 ❏ プロダクトアーキテクチャ設計 ❏ 逆に初期を乗り越えればメリットも多い ❏ 負荷試験でもほぼ調整なしで希望しているパフォーマンスがでていた