C言語でデータベースサーバを書く --設計編--

記事
IT・テクノロジー
凄いデータベースサーバを書く
さて、このブログのシリーズ全体でNFTマーケットのプラットフォームをC言語でスクラッチから書いてしまう、という企画なので、ここで書くのも、すごいデータベースサーバです。
こちらの最初のブログでは、WEBとは何か、ということで、概論を書きましたが、そこでWEBサイトにはデータベースシステムが不可欠で、一般的にはSQLが使われていて、で、データベースをソケットで叩いてWEBページを作り出すのが、CGIで、それは普通はPHPとかrubyとかperlなどのスクリプト言語で書かれている、という話をしました。
で、このブログでは、弊社がスクラッチから開発したHOTPortという(広い意味での)NFTマーケットのプラットフォームでは、CGIは、ほとんど何もせず、ブラウザからのリクエストや送られてきたデータを、データベースサーバに丸投げして、データベースサーバから返ってきたデータを、そのままブラウザに丸投げする、まあ、かなりズボラなCGIについてご紹介しましたね。

となると、そのズボラなCGIが叩くデータベースサーバは、本来ならCGIがやるようなことを全部やるような凄いデータベースサーバになります。
具体的には、ブラウザからCGI経由で送られてきたリクエストを解析する。ついでブラウザからCGI経由で送られてきたデータを解析する。そして、そのリクエストやデータからWEBページを作ってそれをCGI経由でブラウザに送り返すということです。

ということは、まず、データベースとはいうけれど、これはCMS(コンテンツ管理システム)を含むし、また、NFTなんで、ブロックチェーンの処理なども行うわけだし、当然、利用する人のアカウントの管理するし、利用する人が投稿したデジタルコンテンツもNFTとして管理するし、なんでもやれる凄いデータベースサーバになるわけです。しかも、アカウントごとのアクセス権限の処理なども全部やらなくてはならないので、これはこれは凄い凄いデータベースサーバになります。ズボラなCGIのおかげで、なにもかもやる、プラットフォームそのものです。

だから、HOTPortという名前になりました。WEBのプロトコルは、HTTPで、HyperText Transfer Protocol なんですが、HOTPortは、Hyper Object Transaction Port を意味します。つまり、ブロックチェーン自体、とそれに格納されるようないろいろな伝票とか、構造をもってまとまった文書(画像や動画やいろいろなデータとあちこちリンクはりまくりのテキストからなるもの)はもはやテキストではなく、オブジェクトと呼ぶべきもの。だから、ハイパーオブジェクトの間のやり取り(トランザクション)のためのポートなんです。

で、ここまでくると、当然SQLでなんとかしようとすると、実行効率がそうとう悪くなるし、そもそもSQLの使い方勉強するの面倒だし、ってことで、全部C言語で書いてしまったわけですね。その代わり、かなり苦労して、完成までにかなり時間がかかり、出来たと思ったあとも、バグがかなりあって、運用開始から1年余り、今、ようやく安定して動いています。

基本設計の基本
前置きが長くなりました。まずは、基本設計を考えてみます。
まず、HOTPortはCGIとソケットで通信します。その際に、で、データベース自体は、CGIからのリクエストに順序よく対応するので、マルチスレッド型のサーバにならざるを得ません。マルチプロセス型だと、同じ一つのデータベースにアクセスするのをコントロールするのが難しいですからね。ところが、まずこのマルチスレッドサーバのプログラミングには落とし穴がいくつかあって、そのことはソケットプログラミングの教科書みたいなものにも書かれていません。そもそも、ソケットプログラミングの教科書はほとんどありません。ググっても数冊、それも多くは前世紀の古い本です。IPv6対応まできちんと書いてあるのが2、3冊。で、これなら行けるだろう、と思って使っていた教科書の通りに書いたら、まんまと落とし穴にハマり、いろいろググったら、英語のページで解決方法が見つかりました。この件はまた次のブログで詳細を書くことにします。

というわけで、HOTPortは、マルチスレッドサーバで、そのメインのプログラムは、CGIからのリクエストをlisten関数で待っていて、リクエストが来るたびにpthread_create関数でスレッドをたちあげて、あとはそのスレッドにリクエストの処理をまかせて(ここに大きな落とし穴がある)、またリクエスト待ちにする、というものになります。そして、立ち上がったスレッドは、CGIとつながったソケットのファイルディスクリプタをつかって、CGIから来るデータを読み込み、それを解読して、データベースの所定の部分をひっぱりだして、そこからページをつくって、CGIに送り返す、という形です。

次に問題なのは、データベースの内容の更新です。更新されている間、更新される部分は、ほかのスレッドからのアクセスをブロックしないといけないので、そこでmutexをつかった排他アクセス管理をしないといけません。また、データベース全体はかなりサイズが大きいので、長時間アクセスされていない部分はファイルにセーブして、その部分のメモリの解放も必要です。ということは、まず、データベース全体を、多数の部分に分割して、分割した部分ごとに、ファイルにセーブし、リクエストがあったら、その部分ごとにファイルからメモリー上にロードし、また、その部分の書き換え(更新)があったら、pthread_mutex_lockを使って、ほかのスレッドからのアクセスをブロックして、書き換えて、ファイルにセーブして、その部分のメモリーを解放して、そのあと、pthread_mutex_unlockでブロックを外す、ということをやらないといけません。そのために、dbData型というデータ構造を定義して、必要なmutexなどをもたせておくようにしました。

マルチスレッド対応のdbData構造体
dbData型の構造体をここに示します。
----ここから----
struct          dbData
{
//この構造体からポインタで示されるデータの実体を格納する
//メモリー領域を管理するメモリーマネージャ MMを宣言。
  memoryManager         MM;
//他のスレッドからのアクセスをブロックするためのmutex構造体の実体
  pthread_mutex_t       Mutex;
//この構造体のデータをアクセスしようとして、待っているスレッドの数を
//カウントする変数 CntWaiting のアクセスをロックするためのmutex構造体
  pthread_mutex_t       MutexCW; 
//この構造体をアクセスしている最中のスレッドの数をカウントする
//変数CntAccessingのアクセスをロックするためのmutex構造体
  pthread_mutex_t       MutexCA;
// CntAccessingと、CntWaitingの実体。
  int32                 CntAccessing;
  int32                 CntWaiting;
//この構造体が何時ロードされて、アクセス可能状態になったかの時間。
  int64                 TimeOpened;
//この構造体の DataBlockに入っているデータ構造を示すstructId型構造体
//へのポインタ。
  structId*             StructId;
//この構造体が格納しているデータの実体、あるいはポインタを格納する
//block型データ。
  block                 DataBlock;
};
----ここまで----
さて、説明します。変数MMは、メモリーマネージャで、必要に応じて、メモリー領域を確保し、足りなくなったら、また追加で確保し、ということができて、かなり便利なメモリー管理です。メモリーを解放するときは、このMMを一気に解放すればよいので、実行効率はかなり速くなるんじゃないかなと思います。
つぎに、pthread_mutex_t型のmutexの実体が三つあります。最初は一つ、Mutexだけしかなかったのですが、どうも、システムが時々固まる、デッドロックが起こるので、原因をしらべたら、MutexCW, MutexCAで、CntWaitingと、CntAccessingの二つの変数もちゃんとmutexでロックしたりして管理しないといけないとわかりました。
で、CntWaiting は、まず、このデータ構造をアクセスしようとするスレッドがこのカウンタを1だけ増やして、待っているよー、ということを宣言するための変数です。これを書き込むときには、MutexCWをロックして、1だけ増やして、アンロックします。
で、Mutexがロックされている間は待っていて、でMutexがアンロックされたら、中に入るときに、MutexCWをロックして、CntWaitingを1だけ減らして、また、MutexCWをアンロックして、中に入ったよ、ということにして、今度は、MutexCAをロックして、CntAccessingを1だけ増やして、MutexCAをアンロックして、アクセス中だというわけです。
内容の更新しないスレッドからのアクセスは同時にいくつでも可能です。でも、内容の更新を伴うスレッドがアクセスするときは、まず、Mutexをロックして、あとから別のスレッドが入れないようにして、そのあと、今アクセス中のスレッドが無くなるまで、つまり、CntAccessingが0になるまで待って、で、そのあと、書き換えをして、ファイルにセーブして、メモリーを解放して、Mutexをアンロックする、という仕組みになります。
で、この仕組みで、しっかりデータベースは管理できて、更新を伴わいアクセスは同時に多数可能だし、更新を伴うものは、しっかり管理して、更新をセーブして、という形にできるんです。

で、この仕組み、一般的なデータベースシステムが普通に使っている方法なのかどうか知りません。SQLがどうしているかもしりません。でも、たぶん、こういう方法でないと、効率良くアクセス制御したりできないと思います。

仮想関数でオブジェクト指向っぽくする structId構造体
で、次に、structId型という構造体は、C++言語で使われている仮想関数ポインタテーブルそのものです。あるオブジェクトがあれば、そのデータ構造を読み込んだり、書き出したり、するための基本的な関数があってそれは共通の仕様になっているので、それらの関数を仮想関数として管理すれば、一応、オブジェクト指向っぽくなります。
ということで、structId型を書いておきます。
----ここから----
struct          structId
{
//構造体のID、64ビットの整数。
  uint64                        StructId64;
//構造体の名前(文字列)
  char*                         DataTypeStr;
//構造体の大きさ
  size_t                        DataSize;
//構造体のコンストラクタ関数へのポインタ
  data_new                      Data_new;
//構造体をメモリー領域から読み込む関数へのポインタ
  data_get                      Data_get;
//構造体をメモリー領域に書き出す関数へのポインタ
  data_put                      Data_put;
//構造体をアクセスする関数へのポインタ
  data_access                   Data_access;
//構造体のデータをクリアしてリセットする関数へのポインタ
  data_clean                    Data_clean;
};
----ここまで----
結局、やって見て思ったのは、オブジェクト指向とかいっても、仮想関数で定義しなくてはいけないのは、この5つくらいの関数だけで、これがちゃんとあれば、充分だということです。で、これくらいの話なら、C++がやっているように、オブジェクトの実体に関数テーブルポインタを持たせるよりも、ポインタをもっている構造体側で関数テーブルポインタをもっていたほうが、アクセス速度はちょっとだけ速くなるんじゃないかと思うわけです。なお、data_getやdata_putは、データのエンディアンを考慮してデータを読み畫きします。すべて、ビッグエンディアンで読み書きします。しかも、プログラム上、エンディアンを意識しないように、シフト演算子でシフトしながら書き出すようにしています。これだと間違いがないです。

いろいろなデータを格納できる blockユニオン
さて、つぎに、dbData型の中にblock というデータ構造があります。これは、128ビット(16バイト)のunionです。 HOTPortでは、ユーザIDとかNFTの IDは、128ビットだし、doubleの複素数(complex128型)は128ビットなので、128ビットあれば、ほとんどのデータ構造は表せます。ポインタも64ビットですしね。現在のCPUはほとんど64ビット型ですが、128ビットにしておけば、数年後に128ビットのCPUとかでてきても、書き換えせずに使えますし。ということで、128ビットまでのデータ構造は、ここに格納可能になっています。

データベースの基本データ構造 hotport構造体
さて、これでデータベースのアクセス管理は万全なので、次はなんでもできる凄いデータベースで必要なデータ構造を考えます。
目標は、ブロックチェーンとCMSをもっているNFTマーケットの分散型プラットフォームのデータベースですから、だいたい次のようなデータ構造が必要です。
まず、データベースのトップです。hotportという構造体で定義しました。
これには、
struct hotport
{
//メモリーマネージャー
memoryManager MM;
//他に管理に必要なデータが多数あるが、省略。
//以下、ファイルで管理すべきデータ
dbData* Portal; // サーバのポータルページのCMS用データ。
dbData* RemoteQueue;//外部サーバアクセス失敗の際のリクエストのキュー
dbData* ExportControl;//海外サーバへの輸出入管理への対応のためのデータ
dbData* UserArray;//サーバ内のユーザ(アカウントをもつ人)の情報の配列
//なお、UserArrayの要素のUserには、投稿されNFT化されたコンテンツの
//配列ProductArrayがある。
dbData* ServerArray;//分散サーバで連結しているサーバの情報の配列
dbData* StatementLogIssued;//サーバ内で発行された決済伝票のログ
dbData* StatementLogPushed;//サーバ内で受領した決済伝票のログ
dbData* TitleArray;//サーバ内のタイムラインのデータ。
}

まあ、Portalは、サーバがアクセスされたときに表示する文書や画像などを収めたデータ構造で、CMSです。これは、実体は、element型という型付きJSONみたいなデータ構造になっています。WEBページの内容は、すべてこの型で表現できます。
RemoteQueueは、分散サーバで、他のサーバとやり取りする際に、その他のサーバがアクセスできない状態(落ちているとか)のときに、とりあえず、送るべきメッセージをここに溜め込んでおいて、次にアクセスできるときに、メッセージを送り出す、というような感じのためのキュー(待ち行列)です。
ExportControlは、輸出入管理の話で、外国のサーバとのやり取りで、相手国の輸出管理の規定によっては、アクセスをブロックしたりする必要があるので、その相手国との輸出管理を制御する仕組みです。
で、 UserArrayは、user型のデータの配列です。user型は、アカウントをもつユーザの投稿したモノ(product型)の配列や、ホームページ(プロフィールページ)、持ち物情報、それから、取引履歴を表すブロックチェーンなどのデータがあります。
ServerArrayは、同じプロトコルで連携している分散サーバをしめすserver型のデータの配列です。サーバのIDとか、URLとか、IPアドレスなどを管理してます。
StatementLogIssuedは、発行された決済伝票のブロックチェーンです。
StatementLogPusshedは、受領した決済伝票のブロックチェーンです。実はふたつは同じものです。
最後は、TitleArrayで、投稿された記事やコンテンツ(タイトルと呼びます)が更新順に入っている配列です。

まとめに変えて
うわー、かなり長くなりました。今回は、データ構造を中心にしたので、無料のブログです。これで、ほぼ、凄いデータベースサーバの基本の基本はデータ構造を示しながら、絞めせたと思うんです。
で、次回は、最初の落とし穴、などと書いたソケット周りのブログを書きます。




サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す