発明のための再発明

Webプログラマーが、プログラムの内部動作を通してプログラムを作る時の参考になるような情報を書くブログ(サーバーサイドやDevOpsメイン)

稼働システムのマイグレーションの痛みを和らげる技法

はじめに

サービスを長く継続すると避けられないのが「マイグレーション」です。
しかし、マイグレーションには多くの手間がかかる上に致命的なバグも出やすく、難しい作業です。
そんなマイグレーションを実践し、詳細を公開している企業があります。
それを参考にどのような工夫ができるのかを見ていきます。

紹介するのは以下の企業です。

各事例には元記事へのリンクを書いているので、興味があればリンク先も覗いてみてください。

食べログ: Railsからマイクロサービスへの移行

元スライド: 食べログのマイクロサービス化PJについて

まずは、食べログのマイクロサービス移行についてです。
移行に際しては、共有しているDBをいきなり物理的に分けるのではなく、はじめに論理的に分割する方法を取ったようです。

※ 2018年9月末時点で進行中なので、現在も進行していると思われます。

移行理由

既存のシステムでは、

  • メインのデータベースが一つ
  • modelやlibをアプリケーション間で共有している
  • デプロイは全アプリケーション同時に行う

というアーキテクチャをもっているため、共有されたデータ構造を変更する際に影響範囲が広く対応が大変だそうです。
そのため、マイクロサービスへと移行し始めたそうです。

移行方法

移行の手順をまとめると、

  • 始めからサービスを細かく分解することを諦める
  • まずは境界が明確な2つのサービス(下図)へと分割し、境界の横断にはAPIを使うように修正する
  • 全サービスがAPIを使用するようにできたら、DBを分割する(予定)

このように、DBの共有を止めたいけれど、ビッグバンリリースでは大変なので、まずは、ソースコードの修正によって論理的にDBを分割します。
その後に、物理的にDBを分けるという戦略です。

The Guardian: MongoDBからPostgreSQLへの移行

元記事: Bye bye Mongo, Hello Postgres

次は、イギリスの新聞社である「The Guardian」がMongoDBを捨てて、PostgreSQLへ移行した方法についてです。
旧コードを本格稼働させつつ新コードにも同内容を実行することで、新旧コードが同じ結果になることを確かめながら移行したようです。

移行理由

The Guardianには記事の管理に使用している内製CMSツールがあり、MongoDBとマネジメントツールであるOpsManagerを使用していたのですが、

  • AWS上に自分たちでMongoDBとOpsManagerを建てて、管理する必要がある (マネージドサービスが使えなかった)
  • 障害時にMongoDBもOpsManagerも助けにならなかった
  • OpsManagerを入れれば管理が楽になると思ったが、そうでもなかった
  • 価格が高い

という要因が合わさり、移行先を探し始めてPostgreSQLへ決めたそうです。
ちなみに、移行時にスキーマを作り直すことはせず、JSONB型の列を用意してMongoDBの内容をそのまま持っていったようです。

移行方法

移行は下の手順で行ったそうです。

  1. 新DB(PostgreSQL)への書き込みAPIを用意する
  2. 前段にプロキシを用意して、新旧両方のAPIに同一のリクエストが流れるようにする(プライマリは旧API)
  3. 新DBへデータを移行
  4. 新旧APIのレスポンスが同一であることをチェック (別の結果が返ってきたら、新DBのデータを巻き戻してやり直し)
  5. APIをプライマリにする
  6. 旧DB(MongoDB)を削除

f:id:mrasu:20190115011442p:plain

プロキシを用意して変更の有無を確認することで、ユーザーに影響を与えずに新APIのエラー検出が出来るようになっています。
また、APIはelasticsearchなどとも連携しているので、APIが間違ったデータを返してしまうと影響が大きかったことからも、慎重に確認したかったようです。

Dropbox: 複数RPCからgRPCへの統合

元記事: Courier: Dropbox migration to gRPC

最後は、Dropboxが行ったRPCの統合についてです。
かつてDropboxは自作プロトコルでのRPCや、ApacheThrift、http1.1でのRPCを使用していたのを、CourierというgRPCを使うための新フレームワークへ移行したそうです。

移行理由

旧RPCフレームワークに変わり、Courierという新フレームワークを作成することで以下のことを可能にしたそうです。

  • サーバー・クライアントを認証・認可するためのDropbox内部にある機構との統合
  • ログや統計などの情報の統合・可視化
  • タイムアウト・サーキットブレーカー設定の強制

移行方法

移行に際しては、旧フレームワークからの移行を開発者が簡単に出来るように注意しつつ、以下のステップを踏んだそうです。

  1. 旧RPCフレームワークのコードのフリーズ
  2. Courierと旧フレームワーク双方を使用できる共通インターフェースの作成
  3. 共通インターフェースを使用するようにコードを変更 (稼働させるのは旧RPC)
  4. 旧RPCサーバーとCourierサーバーを同時稼働
  5. クライアントをCourierへ切り替え
  6. 旧コードの削除

フレームワークへの機能追加を止めることで開発者たちへ移行のインセンティブを与えたことと、新旧フレームワークを同時に使えるようにして順次切り替えを行い「移行」に対するリスクを抑えたという、工夫が見れます。

まとめ

以上、各企業がマイグレーションをした方法とその理由を掲載している記事を紹介しました。

このブログでは、この記事のようにプログラムを作る時の参考になることを書き続けるつもりです。
もし興味があれば、twitterやブログのフォローしていただけると嬉しいです。

分散ストレージ Ceph - "Ceph: A Scalable, High-Performance Distributed File System"

はじめに

分散ストレージであるCephについての論文を読んだので紹介します。

最近CybozuのNecoプロジェクトが始まり、面白そうなことをやっているな、と思っていたたところ、Cephについてのブログが出ました。
これを読んで、「Ceph読んでみたいな」と思ったので、理解の足がかりになるかなと思い論文("Ceph: A Scalable, High-Performance Distributed File System")を読みました。

ただし、この論文は2006年に発表されたもので、当時のCephの概要について説明されたものです。
現在の実装とは大きく違うと思います。
たとえば、この論文では「CephはEBOFSという独自ファイルシステムを採用している」とあるのですが、GitHubにはそのようなものは存在せず、2009年ごろに "osd: do not use ebofs"というコミットとともに削除された形跡が伺えます。
それでも、分散ストレージは「自分で作ってみたいシステムランキング」の上位にあるものなので興味深く読みました。
また、論文に出てくる「CRUSH」と「RADOS」はデータの配置と冗長化という、分散ストレージに欠かせない仕組みで、Cybozuのブログを読む限り今も現役なようです。

Cephについて

Ceph自体の説明については、
Cybozuの記事にまとまっているので、そちらが参考になります。: https://blog.cybozu.io/entry/2018/12/13/103039
公式: https://ceph.com

ちなみに、CNCFのIncubating ProjectになっているRookもCeph連携をしています。

論文の概要

"Ceph: A Scalable, High-Performance Distributed File System"では、Cephにおける

  • メタデータの管理
  • データの管理
  • クライアントとサーバーの関係

という、おおよそCephの全体像を書いています。
簡単にまとめると、
Cephは

で構成されていて、クライアントはこの2つのクラスタに問い合わせることでファイル情報や内容をやり取りします。
MDSクラスタの大きな特徴は、「ファイルの位置情報」を保管せずに、計算によって対象ファイルを保持するOSDがわかるということです。
また、OSDクラスタは冗長、耐障害を考慮された設計です。

OSD(object storage devices)とは、ディスク(またはRAID)と、それに付随するCPU・ネットワーク・キャッシュを含むデバイスの呼び方です。長いですが、「ファイルコンテンツの一部を持ったデバイスOSDというだな」という理解で十分だと思います。

以下、詳細です。


System Overview

この章は、Ceph全体についての説明です。次章以降、各機能が説明されます。

メタデータの分離

Cephでは、open, renameなどのメタデータに対する操作はメタデータサーバー(MDS)で一括管理されていますが、read,writeのようなIO操作はクライアントとOSDが直接通信するようになっています。
また、データがどのOSDに格納されているかという情報はメタデータサーバーでは管理していません。その代わりに、CRUSHを使うことで場所が計算できるようになっています。

メタデータの動的管理・分散管理

メタデータに対する操作はCeph全体の負荷の半分にもなる可能性があるので、メタデータを効率的に管理したいところです。そのために、Dynamic Subtree Partitioningという手法を使用しています。

RADOS (Relaiable Automatic Distributed Object Storage)

Cephは数千を超えるデバイスを持つシステムなので、以下のことが想定されます

  • バイスが追加、削除される
  • 故障が頻繁に起こる
  • 大きなデータが追加・移動・削除される

なので、Cephはデータのmigration,replication,failure detection, failure recoveryの機能を持っています。(詳細は後述)

クライアント操作から見る Ceph

capability

クライアント操作時には、"capability"という操作許可が各クライアントに発行されます。
例えば次のようなcapabilityの移動があります。

read時:
クライアントがread用にopen操作をMDSに要求すると、MDSがreadのcapabilityを与える。
capbilityを与えられたら、クライアントはメタデータ情報を使って、OSDクラスタへアクセスして、データを取得する。

write時:
writeのためのopenの場合には、writeのcapabilityを与える。
その後にクライアントはデータを変更して、closeする。
close操作の時に、MDSはファイルサイズを更新して、capabilityを破棄する。

同期

POSIXでは、読み込み時には書き込み済みのデータを読み込むこと、書き込みはatomicであることを要求しています。つまり、操作は発生順(order of occurrence)に結果を持つことを要求しています。
しかしCephでは、「複数書き込む場合」や、「書き込みと読み込みが同時に発生した場合」には、キャッシュ読み込みとバッファ書き込みのcapabilityが取り消されて、各操作の同期が強制されます。
この方法は、同期IOになるので遅くなりますが、通常のユースケースでは読みと書きが同時に起きることは少ないので、許容できると判断しています。
ただし、許容出来ない場合に備えて一貫性を犠牲にする選択肢も有るようです。

名前空間に関する操作

名前空間に関する「読み(readdir, statなど)」、「書き(unlink,chmodなど)」の操作はMDSに対して行われますが、ロックはありません。これは、シンプルさと最適化を求めた結果だそうです。
例えば、ls -lのようなreaddir+statの操作はよく実行される操作ですが、巨大なディレクトリに対してはパフォーマンスキラーです。
なので、デフォルトではキャッシュが使われます。
そのせいで、一貫性が損なわれますが、パフォーマンスのためには歓迎される犠牲だとして採用しています。

動的に分散されるメタデータ

Cephのメタデータは分散されつつも動的に場所が変わり、以下の特性があります

  1. ディレクトリのコンテンツは同一のOSDクラスタにある
  2. MDSに配置されるデータはアクセス量に従って動的に変化する(どこかのMDSにアクセスが偏る場合は、一部のディレクトリを別のMDSに移動します)
  3. メタデータの一貫性のポリシーは、security(ownerやmode),file(size,mtime),immutable(inode number, ctime, layout)の3種類存在し、目的に適したものを使っている。
  4. 大量のアクセスが同一のディレクトリやファイルに来た場合には、レプリケーションを拡大して、クライアントにレプリケーションへアクセスするように指示する

Distributed Object Storage

クライアントやMDSにとって、OSDクラスタは論理的に一つのストレージであるとみなして扱います。
それを実現するために、以下の工夫をしています。

CRUSH (Controlled Replication Under Scalable Hashing)

ファイルはobjectに分解され、objectはPG(placement group)というグループに割り当てられます。PGはCRUSHを使用して、各OSDに割り当てられます。
CRUSHでは、PGとPGに紐づくOSDのリストがあればデータを持つOSDがわかるようになっています。
この仕組みのおかげで、クライアントやMDSは独立して保存場所を計算することができ、メタデータが持つ情報の更新が少なく済むようになっています。
つまり、CRUSHでは、distribution(どこにデータを置くべきか)とlocation(どこにデータが有るか)の問題を同時に解決しています。

レプリケーションとデータ保全

データは複数PGにレプリケーションされます。
クライアントがプライマリPGのOSDにデータを送ると、対象のOSDは受け取ったデータをレプリケーションにも流して、待機します。
そして、レプリケーションの書き込みが終わってから、クライアントに完了を知らせます。
こうすることで、複数デバイスへの書き込みが保証されます。

Failure detection

ディスク障害などはODSから障害を通知しますが、ネットワーク障害の場合には各OSDのピアが生存確認できなくなった時に、中央に報告が上げられます。
そして、中央がシステム障害なのか一時的なものなのかを判断します。
Cephにはdownとoutの2つの障害状態を用意していて、OSDが通信できなくなるとdownとなりprimaryから外されます。そのままdownの状態が続くようだと、outの状態に遷移してPGには別のOSDが割り当てられます。
このように状態をもつことによって、(停電でOSDの半分がダウンするなど)大規模な障害が起きた時に状態を"down"に留めることによって、大規模なデータの再配置を避けることができるようになっています。

OSDクラスタ情報の更新

Cephの持つクラスタ情報がOSDの追加や削除によって変更されると、各OSDは自身の情報との差異に気づき次第、OSDの「あるべき姿」へと変化します。(「あるべき姿」になるために、プライマリの変更やデータの移行などが発生します)
このように、各OSDは独立して変化するので、あるOSDが落ちた場合には、影響を受けた各PGは並行して復帰します。

EBOFS (Extent and B-tree based Object File System)

Cephでは、メタデータとデータのatomicな操作が出来なかったので、ext3(古い!)のような既存のファイルシステムは使わずにEBOFSというものを作ったそうです。
EBOFSは以下の特徴を持ったファイルシステムです。

  1. atomic transactionをサポートすると同時に、ディスクへの書き込みは非同期に行われる。
  2. (既存のファイルシステムが時間を開けるのに対して)ディスクへのflushは積極的にスケジューリングされ、IO操作が不要になったときにはキャンセルすることもできる。
  3. オブジェクトのディスクへの配置、block allocation, index collectionには、B-treeが使われている。

終わりに

以上、"Ceph: A Scalable, High-Performance Distributed File System"の内容でした。
今回の記事のように、このブログではプログラムを作る時の参考になることを書き続けるつもりです。
もし興味があれば、twitterやブログのフォローしていただけると嬉しいです。

巨大企業のサーバー構成や内部ツールを覗く

はじめに

この記事は設計・アーキテクチャ Advent Calendar 2018の1日目の記事です。

大きなサービスを支えるのは一筋縄では行かず、考えることは多くあります。しかし、ありがたいことに巨大な企業の中にも自社のサーバー構成やそれを支えるツールを公開している企業があります。
この記事では、彼らの叡智に触れるため、有名企業の事例を取り上げ要約をします。
各事例には元記事へのリンクを書いているので、興味があればリンク先も覗いてみてください。

※新しいものばかりではないので、古くなっていたり既に別の方法に移行している可能性があることに注意してください。


LINE: 25k/secのスパイクをさばくアーキテクチャ

元記事: 25K request/secをさばいた「LINEのお年玉」のアーキテクチャの裏側

最初に紹介するのは、LINEが2018年に実施した、「LINEのお年玉」というイベントをさばいたアーキテクチャです。
この記事では、アーキテクチャの他にイベントで発生した問題についても書かれています。

Kafkaを軸とした多段構成

f:id:mrasu:20181130233406p:plain

「LINEのお年玉」では、即時に反映する必要が有る処理を前段で受けつつ、非同期でも問題ない処理をKafka(Streams)に流す構成を取ったそうです。
「LINEのお年玉」はスパイクが見込まれるイベントであることから、キューを挟むことによって、前段の負荷を減らして遅延処理できるように注意した構成ですね。
また、キューとは別にバッチ処理も行ったようです。
仕様技術はKafka, Prometheus, Fluentdです。

WeChat: アクションに応じた負荷制御システム DAGOR

元記事: Overload Control for Scaling WeChat Microservices

次は、中国でLINEのようなサービスを展開するWeChatです。
WeChat内で実装されている負荷制御に関する論文の内容が興味深かったので紹介します。
※ 元記事を読んだのではなく、The morning paperにあったまとめを読んだだけなので、原文とは異なった解釈をしているかもしれません。

負荷が高くなった場合は、優先度の低い行動に対する応答を止める

LINEと同じく、WeChatでも正月(春節旧正月)のアクセスは平常時を大きく超えるらしく、DAGORという負荷制御システムを実装して負荷に対応しているそうです。
DAGORは事前に各サービスの優先度を設定し、負荷の高まりを検知した時に

  • 処理可能な基準を変更することで、特定の優先度以上のサービスへの処理は止める
  • 大きなボトルネックが特定の優先度上にある場合、ユーザー単位に優先度を割り当てることによって処理を止めるユーザーを選択する

という動作をします。
下の図の矢印部分に制御が入るイメージです。

f:id:mrasu:20181130233835p:plain

これによって、「支払い処理は動作するが、動画のアップロードは出来ない」という制御を自動化で出来ると共に、サービス間連携の流れを把握せずにDAGORの運用することが可能になります。
ちなみに、ユーザー単位に優先度を設定することに関して、セッション単位に優先度を割り当てても良さそうな気がします。しかし、セッションを使用すると、ユーザーが「エラーが出たら再ログインし続ければ治る」ということに気がつき、再ログインを繰り返してしまうそうです。結果UXの低下につながるので、セッション単位での優先度割当は使わないそうです。

Salesforce: マルチテナントを支えるDB運用

元動画: Salesforce Multi Tenant Architecture: How We Do the Magic We Do
別スライド: SlidShare

次は、マルチテナントの運用で有名なSalesforceです。
SaaS企業ではほぼ必須になるマルチテナントですが、Salesforceは奇抜なデータ構造を持っています。
この発表では、彼らの「データとメタデータを分けたテーブル設計」とそれに伴う処理方法や、彼らの持つスケーラビリティ性を説明しています。

DataTables vs MetadataTables vs SpecializedPivotTables

f:id:mrasu:20181130234451p:plain

通常のテーブル設計では、テーブルは「事前に決めた対象分野のデータ」を入れるためにあると考えますが、Salesforceではあらゆるデータを放り込む「Data Table」と、そのテーブルの行と列の「意味」を保持する「Metadata Table」に分かれています。
さらに、インデックスや制約を保持する「SpecializedPivotalTable」なども存在します。

つまり、DataTableの各列に意味はなく、列名もテナントのidなどを除いて、Value1,Value2,Value3...となっています。Value1が何を表すかはMetadataTableを参照することで確認できる様になっています。
この構成により、どんなデータにも対応可能になり、柔軟性を発揮しているそうです。

Dropbox: 全文検索エンジン Nautilus

元記事: Architecture of Nautilus, the new Dropbox search engine

次は、Dropboxが内部で使用している全文検索エンジンNautilus」についてです。
Dropboxともなるとデータは地理的に分散されています。加えてユーザー毎にシャーディングするわけに行かないせいで、検索が大変そうです。

Indexing vs Serving

Nautilusは「Indexing」と「Serving」の2役に分かれて動作しています

f:id:mrasu:20181130234625p:plain

Indexingは、保存しているドキュメントからメタデータを生成して検索出来るようにすることが仕事です。
メタデータは、「ドキュメントから直接取り出したもの」と、「そこから更に加工されたもの(加工するものをannotatorという)」で構成されています。
さらにメタデータ作成とは別に、転置インデックス作成のためのoffline buildが別のタイミングで実行されます。
メタデータ作成とインデックス作成が分離しているので、新規annotatorに対するカナリアリリースや既存ドキュメントへの反映が、特別な処理をすることなく実現できています。

ServingではOctopusというシステムを中心に、

  • Nautilus上での検索
  • 外部サービス(Dropbox paperなど)への検索依頼
  • 結果のランク付け
  • アクセスコントロール

が実行されます。
このように、IndexingとServingの2役に分割されていることで、両者が独立して動けるようになっています。

Netflix: コンテナマネジメントシステム Titus

元記事: Titus: Introducing Containers to the Netflix Cloud

最後はNetflixが内部で使用しているコンテナマネジメントツール「Titus」です。
NetflixAWSを使用していることから、Kubernetesとは違い、AWSの各種サービスと連携する事を前提としています。

既存システムを考慮したコンテナ移行

Netflixがコンテナへ移行する際には、既存インフラとの連携や「小さく変える」というのが重視されたようで、Titusでは以下を考慮して作られたそうです。

  • 変更無しで、既存のアプリケーションがコンテナ上で動く
  • コンテナに乗せたアプリケーションが簡単に他のアプリケーションやAWSサービスに繋げられる
  • バッチやジョブが同じリソースプール上で動く
  • 効率的で信頼性がある

Titusは、EC2インスタンス内で動いている「Titus Agent」と、インスタンスにコンテナを配置する「Titus Master」、外からリクエストを受け付ける「Titus API」で構成されています。

f:id:mrasu:20181130234707p:plain

ミドルウェアでは、ZookeeperがMasterのリーダー選出を、Cassandraが永続化を担当しています。

AWS連携

TitusはAWSのサービス(S3やSQS)を使うため、各コンテナのIAMを管理したいところですが、IAMはインスタンス単位で制御されています。
そのため、TitusAgentがプロキシとなり、各コンテナに必要な情報のみをコンテナに渡しています。
またコンテナのIPについて、各コンテナは同一VPC内の固有IPを割り当てられます。それによって、ポート管理やゲートウェイ管理、セキュリティグループ管理が簡単になっています。

他にも、ワークロードの違うアプリケーションを両立させる管理方法や、既存のデプロイツール(Spinnaker)への連携、CloudWatchに連動した独自オートスケール管理といった興味深いトピックが以下の記事で公開されています


まとめ

以上、巨大企業のサーバー構成や内部ツールに関する記事を紹介しました。
今回の記事のように、このブログではプログラムを作る時の参考になることを書き続けるつもりです。
もし興味があれば、twitterやブログのフォローしていただけると嬉しいです。