cybozu.comにおけるマルチテナンシー

クラウド基盤本部のPlatformチームの昆野です。

私たちPlatformチームでは、サイボウズが提供するクラウドサービス「cybozu.com」が安定稼働するためのプラットフォームの開発/運用に取り組んでいます。

今回の記事では、私たちのチームがプラットフォームの安定稼働に向けて取り組んでいる内容をご紹介します。ただし、私たちの取り組みは多岐にわたっており、すべての取り組みを説明するのは難しいため、今回はこれまでに取り組んできた「リソースの収容効率を改善するためのアーキテクチャ改修」について、重点的にご紹介します。

cybozu.comについて

まず、cybozu.comとはどのようなサービスであり、またその内部構成がどのようになっているのかについて説明します。

サービス概要

私たちが提供している「cybozu.com」について、簡単に説明します。cybozu.comではkintoneGaroonOfficeMailwiseといったアプリケーションをサービスとしてお客様に提供しています。

お客様はcybozu.comのサービスを利用するにあたり、cybozu.com Storeを通じて利用申し込みを行います。この時、お客様は自身でサービスへアクセスする際に使用するサブドメイン(「(指定したサブドメイン).cybozu.com」でサービスにアクセスできるようになります)を指定します。

cybozu.com側ではこの申し込みを受理すると、バックエンドのシステムで各サービスのセットアップ(ミドルウェアの初期化など)を行い、サービスを利用可能な状態にします。すべてのサービスのセットアップが完了するとその旨がお客様にメールで通知され、通知を受け取ったお客様は事前に指定したサブドメインを使用してサービスにアクセスできるようになります。

システム構成

cybozu.comでは複数のアプリケーションをサービスとして提供していることを説明しましたが、各製品のアプリケーションサーバ(AP)単体でサービスを提供することはできません。cybozu.comのクラウド基盤上にはAPの他にデータベース(DB)やElasticsearchなどのミドルウェアコンポーネントが稼働しており、各サービスはAPとこれらのミドルウェアが協働することによって利用できるようになります。

各製品のAPや各種ミドルウェアなど、サービスを構成するコンポーネントはVMやコンテナの形式で稼働しています。私たちは1つ1つのVMまたはコンテナをインスタンスと呼称しています。cybozu.comは大規模なサービスであり、各コンポーネントごとに大量のリソース(CPU、メモリ等)が必要となることから、基盤上には各コンポーネントのインスタンスが多数稼働しています。

そして、cybozu.comではこれらのインフラリソースをお客様が共有するモデルによってマルチテナントSaaSを実現しています。マルチテナントというのは単一のシステムを複数のテナント(=お客様)が使用できることを意味します。また、マルチテナントなサービスは複数のテナントが利用する一方で、各テナントからの視点では自身が使用している環境は独立であるように見える(他のテナントが使用している環境を参照したり、操作することはできない)という特徴があります。

cybozu.comでは複数のテナントが単一のAPやDBのインスタンスを使用するアーキテクチャにすることでインフラリソースを分配しています。また、このようなアーキテクチャにすることで基盤上のリソースを有効活用したり、各コンポーネントのセットアップ時間を短縮することを実現しています。

インフラリソースの分配の仕方

従来のアーキテクチャ

従来のアーキテクチャでは、サービスの提供に必要なコンポーネント(各製品のAPやDBなど)のインスタンスを特定の構成で一組にまとめたサービスセットというアプリケーションプレーンの単位が定義されています。言い換えると、サービスセットが一組あればcybozu.comの各アプリケーションを動作させることができます。実際にはcybozu.comの稼働に必要なリソースを単一のサービスセットのみで補うことはできないため、私たちは基盤上に多数のサービスセットを構築してサービスをスケールしています。

私たちは個々のサービスセットに対して下記の制約を設けています。

  • 各サービスセットは原則同一の構成をとる。つまり、すべてのサービスセットは同じコンポーネントで構成され、各コンポーネントのインスタンス数は同一である。
  • サービスセットに所属するインスタンスは、同じサービスセット内に所属するインスタンスにのみ依存する

そして、cybozu.comでは各テナントに付与したID(以降、テナントIDと呼びます)にサービスセットをマッピングしており、各テナントはマッピングされたサービスセットのインスタンス(が持つリソース)を使用するアーキテクチャとなっています。そのため、複数のテナントに一つのサービスセットをマッピングすることで、サービスセット内のリソースをテナント間で共有させることができます。

サービスセット

この方式には、各サービスのセットアップにかかる時間を短縮できるというメリットがあります。お客様はcybozu.comの契約後、いずれかの稼働中のサービスセットを使用することになるため、新たにサーバやインスタンスを起動・構築することなくサービスのセットアップが行われます。そのため、お客様がサービスを契約してから実際に利用できるまでの時間を短縮することができます。

サービスセットを導入したアーキテクチャの課題

サービスセットを導入したアーキテクチャでは、個々のサービスセットの構成を自由に変更できないことによって、特定コンポーネントのリソースを柔軟に調整することが難しくなっています。例として、あるサービスセットではAPインスタンスのリソースは余っているのに、DBインスタンスのリソースだけが不足しているというケースについて考えます。この時、DBのリソースを増強する方法として下記の案などが挙げられますが、いずれも十分な対策にはなり得ません。

  • 単純にDB VMのリソースを増強する
    • 物理サーバが持っている以上のリソースは付与できないので、一定以上のリソースは付与できない
  • サービスセット内のDBインスタンスの数を増やしたり、他のサービスセットのDBインスタンスを使用する:
    • サービスセット内であるアプリケーションに対応するDBは一つとすることで接続先が一意に定まるというアーキテクチャであるため、増やしたり他のサービスセットのインスタンスを使うことはできない

こうなると新たなサービスセットを構築してDBインスタンスの数を増やすといった方法でしかDBのリソースを補えません。しかし、サービスセットの構築時にはリソースが余っているAPなどのコンポーネントも一緒に構築することになるため、ピンポイントでDBのリソースのみを増強することはできません。このように、サービスセットを導入したアーキテクチャでは、特定コンポーネントのリソースを増強しようとすると、結果的に他のコンポーネントのリソースも余分に増強されてしまいます。

そして、cybozu.comではこのような特徴によって運用コストが上昇してしまうという課題がありました。従来のアーキテクチャでは上記のケースのように、リソースがひっ迫しそうになると新たにサービスセットを構築してサービスをスケールさせてきました。ここで、cybozu.comはオンプレミスのサービスなので、サービスセットの構築時には物理サーバを購入することになります。この時、全コンポーネント分のリソースを確保できるように、リソースが余っているコンポーネントの分まで余分に物理サーバを購入することになります。

さらに、私たちはこれまでに多数のサービスセットを構築してきました。そのため、かなりの数の物理サーバを余分に購入してきたことになり、運用コストもそれだけ増大してしまいました。もしもサービスセットをまるごと構築せずにDBのリソースを柔軟に調整できるようなアーキテクチャになっていれば、基盤上の物理サーバの数を減らすことができ、運用コストの上昇幅も抑えられたと考えられます。

新しいアーキテクチャ

cybozu.comのクラウド基盤はNecoプロジェクトにより大幅に刷新されることになりました。これに伴い、私たちは従来の課題を解消できるようにアーキテクチャの改修を進め始めました。

柔軟にスケールできるアーキテクチャ

私たちは各コンポーネントのリソース調整を柔軟に行えるように、サービスセットの制約が取り除かれた新しいアーキテクチャを設計しました。新しいアーキテクチャの特徴は以下の通りです。

  • 構成を柔軟に変更できる

    • 各コンポーネントのインスタンス数を任意に増減できる
    • 必要に応じてDBインスタンスのみを追加するなど、部分的なスケールが可能である
  • 依存関係を動的に変更できる

    • 各インスタンスは任意のインスタンスに依存できる
    • 依存関係は動的に変更できる
      • 例:APインスタンスが使用するDBインスタンスを切り替えたり、1つのAPから複数のDBを利用することができる

新しいアーキテクチャ

このようなアーキテクチャでは、インスタンス数を自由に増減させることで、各コンポーネントのリソースを柔軟に調整できるようになります。

依存関係の管理

新しいアーキテクチャではインスタンス間の依存関係を管理する仕組みが必要になりました。例えばあるAPがあるテナントのデータが入ったDBにクエリを投げたいケースについて考えます。このときAPはどのDBインスタンスに接続すればよいでしょうか?サービスセットの場合、答えは自明です。サービスセット内にはDBインスタンスは一つしかないためです。しかし新しいアーキテクチャではそうはいかないため、何らかの方法でこのDBを特定できるようにしなければなりません。

そこで、新しいアーキテクチャではサービスセットに代わってテナントIDを用いて依存関係を管理することを考えました。具体的には各コンポーネントのインスタンスやクラスタ(=同じ機能を提供するインスタンスの集合)は同一テナントIDが紐づいたものにのみ依存するようなアーキテクチャにしたいと考えました。そして、これを実現する手段として、サービス間通信(=インスタンス同士が行う通信)の仕組みを導入することを検討し始めました。私たちが検討してきた方式および、それぞれの方式を採用した場合のメリット・デメリットを説明します。

ゲートウェイ方式

クライアントがゲートウェイにリクエストを送ると、ゲートウェイが適切なサーバにリクエストをプロキシする方式です。

メリット

  • クライアントは通信の際にリクエストをゲートウェイに送るだけでよく、特別な処理を行う必要がないため、クライアント側で考慮すべき事項が少ない
  • すべての通信はゲートウェイを経由するため、認証・認可やログ収集、レート制限などをゲートウェイ一箇所で行える

デメリット

  • ゲートウェイの障害時には通信が全断する可能性があるため、ゲートウェイがcybozu.comの単一障害点となり得る
  • ゲートウェイのパフォーマンスが低下すると通信が遅延する
  • cybozu.comではサービス間通信は様々な通信プロトコルで行われるため、すべてのプロトコルをサポートしなければならず、開発・カスタマイズのコストが高い

サービスディスカバリ方式(DNS)

サーバのホスト名とエンドポイントのIPアドレスがマッピングされたDNSレコードが各サーバについて作成されており、クライアントは名前解決によって接続先のIPアドレスを取得できる方式です。

メリット

  • 既存のDNSインフラを活用できるため、導入コストが低い
  • 標準的な仕組みのため、運用や管理がしやすい

デメリット

  • DNSのホスト名には文字数制限(1ラベル63文字以下)があり、サーバ側のホスト名に様々な情報を含めることができないため、テナントIDなど複数の情報を使って接続先の解決を行うことができない可能性がある
  • 名前解決ではIPアドレスしか取得できないため、スキームやポートなどの情報は別で管理しなければならない*1

サービスディスカバリ方式(独自プロトコル)

HTTPやgRPCなどのプロトコル経由で、各サーバの接続先の情報を保持しているサービスディスカバリから独自の形式で接続先を取得する方式です。クライアントはサービスディスカバリからサーバ側の接続先を取得することができ、その接続先へリクエストを送ることで通信が行えます。

メリット

  • スキームやポートなどの情報をレスポンスに含められる。
  • クライアントは接続先情報をキャッシュできる。そのため、キャッシュされた情報を使用すれば一時的にサービスディスカバリが動作しなくなっても通信を継続できる
  • サービスディスカバリは通信そのものに関与しないので、通信プロトコル別の作り込みが不要

デメリット

  • サービスディスカバリは多くのコンポーネントから使用されるため、負荷対策(クライアント側でのキャッシュ管理など)を適切に行わないと負荷が上昇しやすく、レスポンス遅延などにつながりやすい
  • サービスディスカバリから接続先情報を取得するロジックの実装や、適切なキャッシュ管理など、クライアント側で様々な対応が必要である

私たちは「サービスの可用性をなるべく落とさず」「スキームやポートの情報も返せる」方式を導入したいと考えていました。そのため、サービスディスカバリ方式(独自プロトコル)を導入することにしました。

cybozu.comのサービスディスカバリ

cybozu.comのサービスディスカバリには下記の要件が求められていました。

  • 各インスタンスはテナントID・依存先のコンポーネント名・アプリケーション名などの情報からサーバ側の接続先を取得できる
    • APやDBなどのインスタンスはアプリケーション別に用意されているため、アプリケーション名による接続先の解決ができる必要がある
  • 独自のロジックを組み込める
    • 並行運用に必要なフォールバックを実装できる
    • 私たちのリリースモデルに即した紐づけモデルを実装できる
  • データを堅牢に永続化できる
    • 接続先をロストするとサービス間通信が行えなくなるため、確実にデータを保持する必要がある
  • 常に一定のパフォーマンスが出せる(一定以上の速度でレスポンスを返す)

これを踏まえて、下記のソフトウェアがcybozu.comのサービスディスカバリとして使用できるかを考えました。

KVS(Key-Value Store)

KVS(Key-Value Store)はユニークなキーとバリュー(値)のペアでデータを保持するデータストアです。具体的なソフトウェアとしてはRedisやValkeyなどが存在します。KVSではキーを使ってバリューのデータを引き出すことができます。そのため、各データのキーにテナントID・依存先のコンポーネント名・アプリケーション名を含めたデータを、バリューにサーバ側の接続先を登録しておけば、KVSをサービスディスカバリとして使用することができます。

しかし、KVSでは要件として挙げた「独自のロジックを組み込む」ことは難しかったため、cybozu.comのサービスディスカバリとして使用することは難しいと判断しました。

既存のサービスディスカバリ製品

HashiCorp社のConsulなどのソフトウェアでは各サービスの接続先を登録および取得できるAPIが提供されているため、これらのソフトウェアはそのままサービスディスカバリとして使用することができます。しかし、このようなソフトウェアでもKVSと同様に「独自のロジックを組み込む」ことは難しく、cybozu.comのサービスディスカバリとして使用することは難しいと判断しました。

内製したサービスディスカバリサービス

cybozu.comのサービスディスカバリとして使用できるソフトウェアを見つけられなかったため、私たちはサービスディスカバリに使えるサービスを自分たちで開発しました。

私たちが開発したサービスではテナントID・依存先のコンポーネント名・アプリケーション名などの情報を入力パラメータとして与えると、インスタンスやクラスタの接続先を取得できるAPIを提供しています。そして、各クライアントはこのAPI経由で取得したサーバ側の接続先を使用してサービス間通信を行うことができます。

使い方

このサービスを使って行われるサービス間通信の具体例を示します。ここではテナントAがkintoneにアクセスしたときに、APがDBに接続するケースについて考えます。このケースではAPインスタンスはサービスディスカバリサービスのAPIを呼び出し、自身が依存するDBの接続先を取得します。APIの入力パラメータは下記のように設定します。

  • テナントID:テナントA
  • 依存先のコンポーネント名:DB
  • アプリケーション名:Kintone

サービスディスカバリの使用例

APはこうして取得したDBの接続先の情報を使用して、DBへの接続を実現します。

また、上記にてAPがDBに接続するケースについて紹介しましたが、APが他のミドルウェア(Elasticsearchなど)を使用する際や、ロードバランサがAPにリクエストをプロキシする際にも同様の接続先の解決が行われます。つまり、このサービスは実際の運用環境において、数多くのインスタンスから高頻度に使用される、コアなサービスとなっています。

データの永続化

このサービスではバックエンドにMySQLを使用してDBにデータを永続化しています。具体的には、サービスのAPIが呼び出されると、バックエンドではMySQLを使って各インスタンス/クラスタの接続先データをDBに読み書きします。私たちはcybozu.comで長らくMySQLを運用してきたため、今後も安定して運用できそうだと考えてこのようなアーキテクチャを採用しています。実際にこのサービスは本番環境で数年間稼働していますが、以下のような工夫により継続的に安定したパフォーマンスを出せています。

  • クライアント側での適切なキャッシュ管理
  • MySQLのリードレプリカの活用
  • Watch API(クライアントに変更差分のみを通知するAPI)の提供

まとめ

今回はcybozu.comにおけるマルチテナントの考え方、および内製サービスディスカバリサービスの導入によるアーキテクチャの改修について紹介しました。これによりcybozu.comのクラウド基盤では柔軟なリソース調整が行えるようになり、運用コストを抑えられるようになりました。今回の記事がマルチテナントアーキテクチャに対する考え方の一助となれば幸いです。

また、これはクラウド基盤の成長に向けて大きな一歩となりましたが、新しいアーキテクチャにも課題はいくつか存在します(例えば、サービスディスカバリサービスは多数のインスタンスからリクエストが大量に来るため負荷が上昇しやすく、負荷対策が必要であることなど)。そのため、私たちは今後も継続的なプラットフォームの改善に向けて様々な取り組みを行うつもりですが、その際に得られた知見なども皆様に共有できれば幸いです。

長文となりましたが、ここまで読んでくださりありがとうございました!

*1:SRVレコードではポートの情報も返すことができるが、クライアントの実装変更が必要となる