巨大モノリスをKubernetesに移行してシングルクラスタ運用にするためにどうしたか

Mercari USの矢口です。みなさん、Kubernetesは使っていますか! 新しいサービスでは利用していても既存のものはEC2やVMやオンプレで自動化やオートスケーリングもぼちぼちということも多いと思います。既存の基盤とKubernetes、それぞれ別で管理が必要になりめんどうだなぁと運用されている方も多いのではないでしょうか。

Mercari USではこれまでVMを利用していたすべてのサービスをKubernetesに移行しました! これにより機能の重複したふたつの基盤の管理から開放され、本質にフォーカスした開発が行えるようになりました。

動機

メルカリでは全社的にマイクロサービスアーキテクチャおよびKubernetesへの移行に取り組んでいます。Mercari USでは日本向けサービスに先駆けてこれを進めていて新規機能はKubernetes上でマイクロサービスとしてデプロイされています。

メルカリでのKubernetesを利用した開発・運用Platformへの取り組みについてはこれらの記事をごらんください。

一方、もともとあった機能のコードベースはVMで構築されています。周辺のツールなどもまったく異なったものとなっていました。デベロッパー・QAには二重の学習コストがかかっており、インフラ・プラットフォームのメンバーも複数ツールをメンテナンスする必要がありました。

VM based Kubernetes based
Deploy Slack bot Spinnaker
Metrics Monitoring Mackerel Grafana & Prometheus
Log Monitoring Elasticsearch & Kibana Cloud Logging
Operation Ask Infra team GitOps by ourselves
QA env for Pull Requests Manual Automated
Autoscaling No Yes

この問題を解決するため、もともとのコードベースであるところのモノリスもコンテナ化してKubernetesに乗せることにしました。ツールセットの統一をすることでよりシンプルな体系ができ、プラットフォームチームも一つの環境向けの改善に注力できるようになります。

プロジェクトの流れ

PoC

計画についてあたため始めた際にちょうどよくUSチーム内でのハッカソンイベントがあったため、それに合わせて試しにKubernetesで動かすデモを発表しました。デモで見せる部分はごく限られたものだったため3人で3日くらい集中して開発したところ動くものができあがりました。

デモの評判は上々でEM、CTOに説明し実際にプロジェクトとしてすすめられることになりました。

現状分析

プロジェクト始動後、まず実施することは現状分析とどう移行するかの設計です。ここでのどの程度システムを把握できるかが計画の精度を決めます。しかしモノリスは大きいのでモノリスと言われているわけで極めて困難な作業でした。関係者へのヒアリング、社内の関係ありそうなレポジトリの調査、本番サーバーへログインしての調査などを積み重ねていくしかないところでもあります。
調査の結果以下のように構成されていました。

  1. メインサービス
    a. HTTP server
    b. Queue worker
    c. Cron job
  2. CS向けサービス
    a. HTTP Server
    b. Queue worker
    c. Cron job

1と2についてそれぞれ3種類のワークロードを確認できました。これらa, b, cはコードベースは共通ですがKubernetesでどう表現するかや、どうmonitoringしていくかも3種類ごとにかなり異なりそうです。

開発環境

まずは開発環境からはじめました。これまでは大量のVMが用意されており、独自の管理画面でボタンを押すことで指定されたインスタンスが起動する仕組みとなっていましたが、完全な冪等性などが担保されているわけではなく頻繁に壊れるといった課題もありました。

Kubernetes上ではこれまで実績のある PR Replication Controller を利用し、GitHubでPRを作成すると自動でテスト環境が構築されるようになっています。またこのツールをIstioを利用して拡張して、PRが作成されるごとにVirtualServiceが追加され https://pr1234.example.com といったドメインでアクセスを可能にしています。これはDNS, Let’s encryptのwild card証明書, Istio ingress gatewayを組み合わせることで実現しています。

CS(カスタマーサポート)向けサービスの移行

本番についてはなるべくリスクの低いものから始めていくこととしました。CS向けのWebサービスは事前にメンバーに移行を伝え、新旧で別のドメインを割り振り、問題があった場合には単に古いドメインを利用してもらうようにすることで、気軽にトライアルを実施してもらえるようにしました。動かない箇所があった場合にはフィードバックをもらい直してくサイクルを回してQAの負荷やCSメンバーの業務への影響を最小限に抑えつつ少しずつ移行をすすめることができました。

Queue worker, Cron job移行

User facingでない機能群です。Queue workerについてはすんなりと行ったのですがCron jobについては並列化されておらずスループットを簡単に増やすことができないため後述するレイテンシの問題に悩まされました。一部Cron jobはDB移行まで残してそのタイミングで合わせて移す対応を行いました。

HTTP Server移行

これまでにHTTP Server以外のすべての機能の移行を完了させ、最後に残るのがHTTP Serverです。事前に開発環境・CS向けサービスそしてQueue worker, Cron jobの移行でほとんどの互換性や環境再現性の問題を修正することができています。

移行ではパーセンテージリリースにより問題が発生した場合の影響をできる限り最小化しています。1%リリースで2週間、その後20%まで引き上げてさらに2週間の様子見をしています。その間に細かな問題やモニタリングの課題点などが浮かび上がり大きなユーザー影響なく修正することができました。

モニタリング

Kubernetes移行においてモニタリングは常に大きなトピックです。インスタンスベースからPodベースでの監視に発想を転換する必要があり既存の監視ツールの再利用が難しいことも多くあります。

USチームではKubernetesでのメトリクス監視にGrafana, Prometheusを、ログ監視にCloud Loggingを主に利用しています。今回はHTTP Status、Response TimeなどについてはすべてIstioから取得した値を利用することとしました。Istioは運用負荷は大きいのですがサービスのPodの中身に依存せずにHTTP, gRPCなどの通信についてモニタリングできるため大変便利です。その他追加で必要になったメトリクスについてはPrometheus exporterを都度作成して対応しました。

クラウドネイティブを想定されいていない設計をどうするか

こういった環境移行では Lift and Shift という方式が一般に言われます。まずはできる限りふるまいを変更せずに移行してしまいその後最適化していけばいいという考え方です。できるだけこうすすめたいものなのですが実際にはある程度は新しい環境のやり方に合わせないと実用的ではない・問題が多すぎると言ったケースが発生します。Kubernetesでの動作が想定されていない振る舞いにどう対応していったか解説します。

Log

もともとのアーキテクチャではFluentdを利用していました。Kubernetesではまずはstdoutにすべて出力してまとめて別の場所(例えばCloud Logging)に送信してから処理するのが一般的ですが、もともとの基盤がだいぶ複雑で送る箇所もアプリケーションログ、分析ログなど用途ごとに様々であったことから一旦は現状を維持することにしました。

Fluentdをsidecarとして置いています。ただしアプリケーションログは他のKubernetesとも同じように扱えるようにしたかったため追加でFluentd containerからstdoutにも一部のログを出力するようにするかんたんなプラグイン(Rubyスクリプト)を追加しています。最初はアプリケーションログはapp containerから直接出したかったのですがApache mod_phpという構造の扱いにくさや既存ログとの共存が難しくFluentdコンテナ側でやるのが楽という結論となりました。

この構成特有の問題としてlogがディスクに保存され容量が増加していくという問題があります。もともとはlogrotateをしていたのですがコンテナ環境でlogrotate daemonをどう動作させるかは少し面倒です。考えた末 Cronjob resource を作成し一日一回 kubectl rollout restart して全podをつくりなおすことにしました。Kubernetesならではのやり方ですね。

デプロイに依存していたAssets buildとCache

メルカリではmaster dataといわれる巨大なデータ郡があります。膨大なブランド・商品名一覧などを含むカタログなど、いくつかのテーブルについてパフォーマンスのため特別扱いをしています。これらは今まではデプロイ時にDBから取得されコードに添付されます。またCS向けサービスではこれを追加した状態でasset compileをしていました。

Kubernetesに移行するとこのデプロイサーバーという存在がなくなってしまい、CIでcontainerをbuildすることになります。CIサービスからDBへアクセスしたくない、コンテナはdev/prod両方で使用するため環境依存を含めたくないといった理由からこのあたりはフローを大きく変更する必要がありました。
一旦はすべての処理を各podの起動時に行うように変更したのですが、asset compileは2〜3分ほどかかる処理のためコンテナ起動時に実施するにはあまりにも時間がかかりすぎます。

最終的にCS向けサービスについては開発チームに協力いただきmaster dataをリクエストがあったタイミングでオンデマンドで取得してファイルやブラウザでキャッシュ仕組みに変更していただきました。これによりasset compileフェイズにおけるmaster dataへの依存がなくなり、起動時ではなくcontainer build時に実行することができるようになりました。

Supervisord

Queueを処理する際にSupervisordを利用して各queueごとにworker processを作成する方式をとっており10種類以上のworkerがありました。素直にやる場合にはsupervisordの利用をやめて各workerをpodとして建てればいいのですが、dev環境において一つ問題がありました。Pull Requestごとに自動でpodが作成されるのですが、その際にHTTP Server用のpodは一つであるのにworker用のpodは10個以上作成されることになってしまいます。Pull Requestは100個ほどあり1000個ものpodが作成されることになってしまいます。リソース消費量などをかんがみてsupervisordの利用を続けることとしました。queueとworkerの関係や実装を見直すことで回避できそうですが大規模な変更となりそうだったため見送りました。

CIでのコンテナビルドの速度

ビルドの速度は非常に重要でproductionへのデプロイの際も、開発でPRをpushした際もCIでコンテナがビルドされてから次のアクションへ移ることができます。ただしモノリスの場合は依存ライブラリやgitで取得するサイズが大きく、ビルドは時間がかかりがちです。開発の体験に直結する箇所であるためできる限りの高速化をしました。

コンテナの最適化と高速化については以前記事を書きましたので良かったらごらんください。
Circle CI でのDocker Buildを超高速化するテクニック (EN: Quick tips for the faster container image building on CircleCI)

Pod内のコンテナ間でのタイミング処理の難しさ

VM上で動作していたものを移植しようとするとどうしても多数のSidecarが必要になる事があると思います。今回の実装でもいくつかのReverse Proxy, FluentedなどをSidecarにする必要がありました。ここで問題となるのはPodの起動、終了の手順です。Kubernetesではそれらを制御する方法がないためPre Stop Hookなどをうまく使って制御してやる必要があるのですが、正しく動作させるには予想以上に時間と手間がかかりました。ここではFluentdを例として上げます。互換性のためFluentdをDaemonSetではなくSidecarとして実装する必要があったのですが、ログ欠損させないためにHTTP ServerとJob (CronJob)でそれぞれ次のような対応を行っています。

HTTP Server

Fluentd container

        lifecycle:
          preStop:
            exec:
              command:
              - sh
              - -c
              - while [ $(netstat -plnt | grep 0.0.0.0:8080 | wc -l | xargs) -ne 0
                ]; do sleep 1; done

8080はHTTP serverが動いているポートです。これのlistenが終わったことを確認してからFluentdをflushして終了するようにしています。

Cron Job

Main container

          - args:
            - |
              trap "touch /var/log/exit/done" EXIT
              main_command
            volumeMounts:
            - mountPath: /var/log/exit
              name: exit-log

Fluentd container

          - args:
            - |
              fluentd -c /fluentd/etc/fluent.conf -p /fluentd/plugins &
              CHILD_PID=$!
              while true
              do
                if [[ -f "/var/log/exit/done" ]]
                then
                  kill $CHILD_PID
                  echo "Terminating the Fluentd process because the main container terminated."
                  break
                fi
              sleep 1
              done
              wait $CHILD_PID
            volumeMounts:
            - mountPath: /var/log/exit
              name: exit-log

volume

          volumes:
            - name: exit-log
              emptyDir: {}

それぞれポートの監視やコンテナ間で共有するファイルを使ってステータスを伝えるという、かなりハックの毛色の強い方法になっています。Sidecarの扱いの改善が早くKubernetesに取り込まれてほしいですね。

レイテンシー・パフォーマンスの問題

今までのシステムとKubernetesが別のクラウド事業者にあったためその移行も同時に発生しています。どちらもOregon RegionにあるのですがRTT 15msと距離があり、移行中はMySQL, Memcacheなどの往復回数が多い通信でのパフォーマンスに悩まされました。日本でTokyo Regionだとなかなか起きないことですね。
DC移行とアーキテクチャ移行を同時にやったため両方の問題が発生してしまっている状態でした。手数は増えてしまいますがVMのアーキテクチャのままクラウド事業者の移行のみを実施する選択肢もあります。Kubernetes移行というテーマからは外れてしまうのですが機会があればレイテンシ対策についても書きたいですね。

移行完了後はDBなどもすべて一つのクラウド事業者にまとまったためパフォーマンスは実施前よりも改善されています。

今後

いままで避けていたさまざまな改善をモノリスにたいしても行えるようになりました。Canaryリリース、Distributed Tracing、他microserivcesとの緊密な連携などを既存のmicroserivces向けの資産を生かして導入していきたいと考えています。
また作業スピードを優先してそのまま移行した箇所をよりKubernetesに最適化した形にしていく、Lift and Shift の Shift をすすめていきたいと思っています。

さいごに

メルカリでは常に最高のサービスを提供できるようインフラからfrontendまですべてをダイナミックに刷新し続けています。この刺激的な環境に興味がある方はよかったらお声がけください!

https://careers.mercari.com/jp/search-jobs/

[US App] とタグがついているものが東京拠点でのUSチームでの募集です!

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加