LiBz Tech Blog

LiBの開発者ブログ

Dartでリバースプロキシを作ってみる

はじめに

低レイヤプログラミングという記事をみて、普段扱っているものが、どのようにして動いているのか理解できていないことが多いなと反省しています。 なので、普段使っているものの仕組みを改めて学んでいこうと思います。

f:id:kazuhisa_o:20190307014643p:plain

なぜリバースプロキシか

元々PHPをやっていたが、転職してからRailsをやることになって、「RailsではWebサーバーとアプリケーションサーバーを分けて動かしているのかー」とやや不思議に思ってました。 理由としては、PHPではApacheがいろいろやってくれていたようなので、あまり意識していなかったからだと思います。

なんとなくの印象では、Webサーバーとアプリケーションサーバーでは「大きく捉えるとリクエストを受けて処理して結果を返すじゃん」とテキトーな理解でした。 nginxが大量のトラフィックを捌くのが得意とか、そういうことは知ってましたが、同じようなものだと認識していました。

LiBでは同じWebサーバー上でnginx + rackで動いていて、nginxをリバースプロキシとして使っています。 そこで、特に何やっているか知っているようで知らないnginxを見てみようと思います。

まぁそれとちょっと新しい言語でも触ってみたいなーって気持ちもあったので、個人的ブームのDartがServerのコードを書けるようなのでリバースプロキシにしました。

そもそもリバースプロキシもなんだっけ?

複数のリクエストを受け付けて、どこに送るか割り振って処理を返している中継サーバー的な感じ

あとは、静的コンテンツのキャッシュとかもやってたような... SSLの設定とかもあったような...

あれ?あと何やってんだ?

wikipediaから要件をまとめる

リバースプロキシ(英: Reverse proxy)または逆プロキシは、特定のサーバへの要求を必ず経由するように設置されたプロキシサーバ。一般的なプロキシとは異なり不特定多数のサーバを対象としない。リバースプロキシは、不特定多数のクライアントから寄せられる要求に対して、応答を肩代わりすることにより特定のサーバの負担を軽減したり、アクセスを制限することにより特定のサーバのセキュリティを高めたりする目的に用いられる。

リバースプロキシ - Wikipedia

いったん、目指すのはリクエストを受けて、アプリケーションサーバーに送って返すことにします。

  • nginxの代わりにリクエストを受け付けてrackにリクエスト/レスポンスを処理する

+で書いてあった要件は下記

  • セキュリティ
  • 暗号化/SSL高速
  • 負荷分散
  • 変化しないコンテンツのキャッシュ
  • 圧縮
  • 速度の調整
  • 仮想的なサーバ統合

ってあるけど、うんうんと思うのもあれば、認識してないのもあった。

ピックアップすると「速度の調整」の役割を担っていたことは認識できていなくて、ざっくり書くと。

直接つなぐと、クライアント側がすべてDLするまでアプリ側も処理が切れない、なので、クライアント側の速度に依存してしまう。 そうすると、unicornみたく受付られる量が少ないと、すぐひっ迫しちゃう。 なので、リバースプロキシ側にリクエスト結果をキャッシュしておくことで、アプリサーバー側は安定した通信状況で処理を完結できる。

f:id:kazuhisa_o:20190307014427p:plain

要件を並べてみると、思ったよりいろいろやってんだな。。。 今回作るリバースプロキシは一旦簡単なものだけにします。

今回やりたいこと

  1. httpリクエスト受ける
  2. リクエストが来たらappサーバーに送る
  3. appサーバーからレスポンスを受け取る
  4. appサーバーからのレスポンスをクライアントに返す

やったこと

Dartの導入手順とかは こちら

リバースプロキシのコード

import 'dart:async';
import 'dart:convert';
import 'dart:io';

const String PROTOCOL = 'http';
const int WEB_SERVER_PORT = 8888;
const int AP_SERVER_PORT = 3001;
const String AP_SERVER_HOST = 'localhost';
// 本当はUNIXドメインソケットがいいな
const String AP_URI = '${PROTOCOL}://${AP_SERVER_HOST}:${AP_SERVER_PORT}';
// RH = ResponseHeader
const String RH_CONTENT_TYPE_KEY = 'Content-Type';

InternetAddress _hostIp = InternetAddress.loopbackIPv4;

Future main() async {
  // httpsのリクエストを受けれるようにする
  var servers = await HttpServer.bind(_hostIp, WEB_SERVER_PORT);
  print('Listening on ${AP_SERVER_HOST}:${servers.port}');

  // リクエストがきたら処理が開始する
  await servers.listen((webRequest) {

    // アクセス先のapサーバーに接続
    // GETリクエストだけ
    HttpClient()
        .getUrl(Uri.parse(AP_URI + webRequest.uri.toString()))
        .then((HttpClientRequest apRequest) {
      // apへの接続が成功した
      // 閉じると同時にレスポンスを返す
      return apRequest.close();
    }).then((HttpClientResponse appServerResponse) {
      try {
        appServerResponse.headers.forEach((String key, List<String> values) {
          for (String v in values) {
            // 元のリクエストからヘッダーを受け継ぐ
            webRequest.response.headers.add(key, v);
          }
        });
        // 返ってきたレスポンスを処理する
        appServerResponse.transform(utf8.decoder).listen((contents) {
          webRequest.response
            ..write(contents)
            ..close();
        });
      } catch (e) {
        // apサーバーからエラーが返ってきたとき
        webRequest.response
          ..headers.add('RH_CONTENT_TYPE_KEY', 'text/html; charset=UTF-8')
          ..write(e)
          ..close();
      }
    }, onError: (e) {
      // apサーバーへのリクエスト自体に問題があったとき
      print('Do not access on ap server. ${e.toString()}');
      webRequest.response
        ..headers.add(RH_CONTENT_TYPE_KEY, 'text/html; charset=UTF-8')
        ..write(e)
        ..close();
    });
  },
      onDone: () => print('System close!'),
      // webサーバーの立ち上げに問題があったとき
      onError: (e) => print('System Error. ${e.toString()}'));
}

RailsをローカルホストのPort3001でテキトーに立ち上げる

$ bundle exec rails s
WARNING: Nokogiri was built against LibXML version 2.9.7, but has dynamically loaded 2.9.4
=> Booting Puma
=> Rails 5.2.2 application starting in development
=> Run `rails server -h` for more startup options
[74016] Puma starting in cluster mode...
[74016] * Version 3.12.0 (ruby 2.3.1-p112), codename: Llamas in Pajamas
[74016] * Min threads: 5, max threads: 5
[74016] * Environment: development
[74016] * Process workers: 2
[74016] * Phased restart available
[74016] * Listening on tcp://0.0.0.0:3001
[74016] Use Ctrl-C to stop
[74016] - Worker 0 (pid: 74078) booted, phase: 0
[74016] - Worker 1 (pid: 74079) booted, phase: 0

DartをローカルホストのPort8888で立ち上げる

$ dart reverse_proxy.dart
Listening on localhost:8888

結果

f:id:kazuhisa_o:20190307015726p:plain
Rails起動

やってみてわかったこと

  • httpを受け付けるのとhttpsを受け付けるのは全然別もの
    • httpsは証明書を読み込んでパスワード入れてーと色々やる必要がある
  • GETとPOSTの処理も別物
    • 考えればわかるけど、パラメータの渡り方とかそれの橋渡し方法とかも変わってくる
  • エラーハンドリングで気にしないとイケない点が多い
    • Webサーバー
      • 立ち上がりに問題があるときは落としてOK
      • 処理中に問題があるときは、なるべく落ちずに復旧してほしい
    • APサーバーHTTPにリクエスト
      • APサーバーとつながらないときは検知したい
      • APサーバーからのリクエスト結果が問題あるときは適切にユーザーに見せたい
    • クライアントに結果を返す
      • 通信を握り続けるのは回避したい
  • リクエストによってレスポンス内容を変える必要がある
    • ページが見つからないときと、画像が見つからないときでは結果を変えたい
    • そもそも画像のときの最適な返しかた、jsonのときの最適な返し方とかも異なる
  • 通信もいろいろある
    • "web socket"とか"unix ドメインソケット"とかとか
    • ロードバランサーを選ぶときに新しい規格が増えたときに、対応しているか?とかも重要になるかも
    • dartはunixドメインソケット実装されてなかった(nativeコード(C, C++)も使えるのでやれなくはない)
  • よくわかんない通信ならではのエラーがある
    • Content-Lengthよりでかいよ
      • decodeしたりしたら書き換える必要がありそう
    • moremore
  • 簡単な動きならDartでサクッと作れる

考察

リバースプロキシについて

  • 特徴的なサービスであれば作ってカスタマイズできる余地が全然ありそう
    • すべてをapサーバーやwebサーバーでやるんじゃなくて、webサーバーで受け取った時点で外部サービスにぶん投げたりできる
  • 考慮点が多すぎるので、枯れた技術を使いたい
  • 新しい規格が増えると、その分考慮しないといけないので、アップデートされ続けているものを使いたい

実際に手を動かして仕組みをなぞってみると、新しい発見があるなぁと思いました。 また気になったことがあったら調査していきたいと思います。 最後まで読んでくれてありがとうございました!