Securityこんにちは。HRBrainでSREをやらせてもらっている@mano_0307です。今年も早いものでアドベントカレンダーの時期になってしまいました。あっという間の年末ですね。昨年まで弊社ではアドベントカレンダーをやる文化はなかったのですが、先月終了間際にダメ元で「やろうよ!」とチームの方々にお声がけしたところ、予想よりも多い方が「書く!」と集まってくれました。僕としては赤井秀一(名探偵コナンの登場人物)を真似して「・・・嫌だ、と言ったら?」と、全員から断られてしまうことも覚悟していたので、とても嬉しく思います。急すぎたのもあって、さすがに25人は集まらなかった・・・。穴開きカレンダーです。会社の人たちにアドベントカレンダー書こうよって提案したら、こんなに直前なのに結構集まってくれた。重要なのは、今回「書かない」という選択をした人たちの決断を尊重すること。そこに分断が生まれたら最悪と言える。「誰かが書かないから、その分Aさんが複数記事を書いた。「・・・なんか書いてないからなんか肩身狭いな・・・」みたいな分断は絶対に、絶対に避けたい。いう考えもあって無理はせず、穴は穴のまま残します。穴だって僕らの建設的な選択の結果です。前置きが長くなりましたが、そういうわけでこの記事はHRBrainアドベントカレンダーの第一日目の記事です。qiita.comHRBrainはマルチテナント・アーキテクチャ弊社が運用しているHRBrainは、クラウド人材サービスです。複数の企業様にご利用いただいている「マルチテナント型SaaS」です。マルチテナント・アーキテクチャのサービスを運用する中で、非常に悩ましいのは「DBをどう管理するか」ということではないかと思います。DBはテナント毎に独立させる?それとも全テナント情報を一つのDBにまとめる?DBを分けようか一つにしようか、悩むポイントを表にしてみました。弊社では、「全テナント情報を一つのDBで管理する」という方法が採用されています。Securityを使ってそのリスクを回避しています。Securityについては、弊社CTOが書いたこちらを是非読んでいただきたいと思います。speakerdeck.comまた、弊社インフラエンジニアのユキチさんが書いたこちらの記事も具体的な部分が書かれていてとても面白いです。times.hrbrain.co.jp当記事は、これらの「続編」という形式を取らせていただきます。setting」です。settingを設定した)コネクションを使う必要があります。そのため、当初はDBコネクションプールごと会社ごとに分ける形で実装していました。ここで問題になるのが、コネクション管理の煩雑さ(先ほどの表ではメリット側に挙げていたはず)もさることながら、まずは「アクティブなコネクション数の上限」です。SQLでは、使っているマシンタイプ(メモリーサイズ)によって上限値が変わります。cloud.google.comアプリケーションはKubernetesで動いており、Podの数を増やすことでスケールアウトできる設計ではありますが、このコネクション数上限がネックとなってきます。コネクションの無駄が多くなります。A社からのアクセスは多く、B社からのアクセスは少ないというとき、A社用のコネクションプールは枯渇!猫の手だって借りたい!😹😹😹B社用のコネクションプールにはアイドル状態のコネクションがすやすやと眠っている😪😴💤という理不尽な状態になるのです。このままではいつかA社用コネクションプールは蜂起します!!(しない)これを避けるために例えば「アイドル状態のコネクションなんかいらないかも!IdleTimeoutを1秒くらいまで短くして、さっさとCloseするようにしよう!!」ということもできます。すると使用するたびに毎回毎回アプリケーションPodとPostgreSQLとのTCPハンドシェイク(「やぁ初めまして。」「つい1ミリ秒前にきたばっか!寝ぼけてるよね!!」)PostgreSQLにログインGoのコネクション実体を生成をやることになります。Securityを使えないか。「コネクションを使う直前に、所属テナント用のroleに(もしくは設定に)切り替えられないか?」という問題の解を見出すことができればいい、ということでした。そこで僕は、Goの標準DBライブラリであるdatabase/sqlのコードを、穴が開くほど読みまくりました。こちらのGo製memcachedクライアントライブラリを実装したときにもコネクションプール実装を真似するために読みまくったのですが、それ以来でした。github.comいずれにせよ、僕はまるでルーペが黒い紙を燃やしてしまうようにGoのコードを見つめ続けました。GoのSessionResetterインタフェースを使うここで簡単に、database/sqlのコネクションプールがどのような実装になっているかを見てみます。Goのコネクションプール実装で骨格となっているのはこのconn関数です。何をやっているかというと、このようなことをやっています。アイドル状態のコネクションがあるとき、そこから一つ拝借するアイドル状態のコネクションがなく、かつすでにactiveなコネクションの数がMaxOpenConnsに到達しているとき、もうコネクションを新規作成することはできないため、誰かがコネクションを使い終わるのを待つ。使い終わったコネクションはchannelを介して渡されるアイドル状態のコネクションがなく、でもまだactiveなコネクション数がMaxOpenConnsに到達していないなら、ここぞとばかりに「よっしゃ!」とコネクションを新規作成するさて、僕が辿り着いたのは、database/sqlに用意されているSessionResetterインタフェースでした。実装すれば、アイドル状態または他の処理にて使用されたコネクションを使う直前に行う処理を実装できます。それだけでは不十分です。「新規コネクション生成時」にも行わないと手落ちになります。Connectorインタフェースも使う新規コネクション生成処理を実装するにはConnectorインタフェースを実装します。https://github.com/golang/go/blob/e5da18df52e3f81534d7cdb6920cf993b5f079d2/src/database/sql/driver/driver.go#L109-L138この二つ、SessionResetterインタフェースとConnectorインタフェースを実装すれば、既存コネクションを再利用するときも、新規作成直後のコネクションを使うときも網羅できます。これらのインタフェースを実装したdriverを使って*sql.DBの実体を生成するには、OpenDB関数をコールします。https://github.com/golang/go/blob/e5da18df52e3f81534d7cdb6920cf993b5f079d2/src/database/sql/sql.go#L714-L743下記の例では、このSessionResetterインタフェースを実装したconnと、Connectorインタフェースを実装したconnectorのサンプルを書いてみました。database/sqlにて、新しいconnectionが生成されるときに呼ばれる。database/sqlにて、ConnectionPoolからconnが取り出されるときに呼ばれる。ctxからapplication_name切り替えに必要な情報を取得する。当サンプルでは未実装。&conn{})このように用意したconnectorをOpenDB()関数の引数に下記のように渡せば、*sql.DBが生成されます。ちなみに下記の例はpqというPostgreSQLドライバーを使用しています。ORMライブラリを使用するとき弊社ではマイクロサービス・アーキテクチャを採用しており、サービスによって使用しているORマッパーライブラリも異なります。弊社で現在メインで使用しているのは、gormとxormです。github.comgitea.comgormを使うときConfig.ConnPoolに、↑のように自分で用意した*sql.DBをセットしてOpenをコールします。xormを使うとき当初、xormにはgormのように、自分で作った*sql.DBを渡すような方法は用意されていませんでした。下記PRを出したところ、すぐにマージしてくれました。gitea.com↑で実装されているNewEngineWithDialectAndDBに、自分で用意した*sql.DBを用意してやればいいです。その他ORマッパーライブラリを使うときも、「このライブラリは自分で用意した*sql.DBをそのまま使えるか!?」という観点で選ぶのが重要です。Securityを実現させることができています。あと小並感ですが、database/sqlの実装を読むのはchannelの勉強にもなってとてもおすすめです。なんとか書けました・・・!自分で言い出しっぺとなったアドベントカレンダー、さすがに初日に穴を開けるわけにはいかなかった・・・でも書くテーマもさっき(11/30深夜)決めたし、なぜかこういう日に限って妻と「世界の成り立ち」に関して長時間話し合った結果時間がどんどんなくなるし・・・よくやったよmanoくん!!😭HRBrainでは、トライドリブンという文化の中、一緒に様々な興味深い難題を解決し続けてくれるエンジニアを募集しています!もう面白い仕事が山ほどあります。