もなかアイスの試食品

「とりあえずやってみたい」そんな気持ちが先走りすぎて挫折が多い私のメモ書きみたいなものです.

SpringBootで接続先のデータベースを動的に切り替える

はじめに

とあるサービスを作成することになり、想定する利用人数をもとに、負荷分散について調べていた。

現状大丈夫そうだなと思っていても、今後利用人数が増えることを考えると、DBの負荷分散はどうしたら良いのか分からなかった。

負荷分散について考える前までは、レプリケーションやら、AWSのリードレプリカの概要・説明が出てくるけど、アプリケーションの変更が必要なのか良く分からなかった。

調べてみると、やっぱりアプリケーションの改造をやるか、PostgreSQLの場合はpgpoolのようなミドルウェアが必要っぽい。

AWS上の環境をあまり増やしたくないので、SpringBootで作成したアプリケーションに、DBの接続先を切り替える機能を作ることにした。


目次


参考サイト

以下のサイトを参考にした。

fedulov.website

qiita.com

javaworld.helpfulness.jp

m-namiki.hatenablog.jp


環境

AWS上で想定している将来の環境は以下の感じ。

f:id:monakaice88:20180728164600p:plain

ということで、開発環境にDNSサーバ・レプリケーションを構成済みのPostgreSQL2台を構築した。

DBのロードバランスは、DNSサーバにお任せすることにした。


作業の概要

いろいろやることがあったので、ざっくり以下にまとめ

  1. コンテキストを保持するクラスを作成
  2. コンテキストより、使用するBean名を返すクラスの作成
  3. 接続先を指定するアノテーションを作成
  4. コンテキストを切り替えるAspectを作成
  5. 読み取り用と更新用とそれぞれのDB接続設定を追加
  6. Beanを作成
  7. コントローラにアノテーションを付与

サービスにアノテーションを付与するようにしたかったけれども、サービスに付与するとテストが通らなった。

コントローラに付与すると問題なく動作し、また既に色々処理が出来てしまっていたので、今回はコントローラにアノテーションを付与ようにした。

(コントローラのメソッドの中で、「読み込み用」のサービスメソッドの次に「更新用」サービスメソッドを呼んでいるのが原因。"このコネクションじゃDBの更新は出来ないよ"みたいなことを言われていた気がする。ビジネスロジックが1つのサービスのメソッドでまとまっていれば、サービスにアノテーション付与出来たと思う)


1. コンテキストを保持するクラスを作成

まずは、Enumを作っておく。

public enum DataSourceType {
    ReadOnly,
    Updatable,
}

接続先の設定を保持するコンテキストクラスを以下のように作成

public class DbContextHolder {

    private static ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();

    public static void setDataSourceType(DataSourceType type) {
        contextHolder.set(type);
    }

    public static DataSourceType getDataSourceType() {
        return contextHolder.get();
    }

    public static void clear() {
        contextHolder.remove();
    }

}


2. コンテキストより、使用するBean名を返すクラスの作成

AbstractRoutingDataSourceを継承したクラスを作成。(定数文字列:READ_ONLY_DATA_SOURCE_NAME、UPDATABLE_DATA_SOURCE_NAMEは別のクラスで定義している)

public class RoutingDataSourceResolver extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        if (DbContextHolder.getDataSourceType() == null) {
            return null;
        }

        switch (DbContextHolder.getDataSourceType()) {
            case ReadOnly:
                return READ_ONLY_DATA_SOURCE_NAME;
            case Updatable:
                return UPDATABLE_DATA_SOURCE_NAME;
            default:
                throw new RuntimeException("unknown datasource");
        }
    }
}


3. 接続先を指定するアノテーションを作成

Controllerクラスのメソッドに付与し、「読み込み用」か「更新用」どちらに接続するかを設定するアノテーションを作成

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {

    DataSourceType value();

}


4. コンテキストを切り替えるAspectを作成

先程のアノテーションをもとに、コンテキストを設定するアスペクトを作成する。

今回はコントローラのメソッド単位で、接続先を切り替えるので、「@Controller」、「@RestController」が付いていないクラスから呼ばれると例外を投げるようにした。

@Aspect
@Component
public class SwitchingDataSourceAspect {

    @Around("@annotation(com.example.sample.aspect.DataSource)")
    public Object proceed(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            DataSourceType type = this.getDataSourceType(joinPoint);
            DbContextHolder.setDataSourceType(type);
            return joinPoint.proceed();
        } finally {
            DbContextHolder.clear();
        }
    }


    private DataSourceType getDataSourceType(JoinPoint joinPoint) throws NoSuchMethodException {
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        String methodName = signature.getMethod().getName();

        Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
        Method method = joinPoint.getTarget().getClass().getMethod(methodName, parameterTypes);

        Controller controller = joinPoint.getTarget().getClass().getAnnotation(Controller.class);
        RestController restController = joinPoint.getTarget().getClass().getAnnotation(RestController.class);
        DataSource dataSource = method.getAnnotation(DataSource.class);

        if (controller == null && restController == null) {
            throw new IllegalArgumentException("@Datasource is only can use in @Controller or @RestController.");
        }

        return dataSource.value();
    }

}


5. 読み取り用と更新用とそれぞれのDB接続設定を追加

SpringBootの設定ファイルに「readOnly」と「updatable」を追加。

spring:
  datasource:
    readOnly:
      driverClassName: org.postgresql.Driver
      url: jdbc:postgresql://read-only-db.sample.internal:5432/sample_db
      username: xxxxxxxx
      password: xxxxxxxx
      minIdlePoolSize: 3
      maximumPoolSize: 40
      idleTimeout_ms: 300000
      maxLifetime_ms: 1800000
    updatable:
      driverClassName: org.postgresql.Driver
      url: jdbc:postgresql://updatable.sample.internal:5432/sample_db
      username: xxxxxxxx
      password: xxxxxxxx
      minIdlePoolSize: 3
      maximumPoolSize: 20
      idleTimeout_ms: 300000
      maxLifetime_ms: 1800000


6. Beanを作成

設定ファイルをもとに、DataSourceを生成するBeanを作成。

DBマイグレーションでFlywayを使用している場合、更新用のBeanに「@FlywayDataSource」を付与する。これがないと起動できなかった。

RoutingDataSourceResolverを返すBean(multiDataSource)に「@Primary」を付与する。これがないと、「multiDataSource、readOnlyDataSource、updatableDataSourceどれを使うんだよ?」的なことを言われて起動できなかった。

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    public static final String READ_ONLY_DATA_SOURCE_NAME = "readOnlyDataSource";
    public static final String UPDATABLE_DATA_SOURCE_NAME = "updatableDataSource";

    @Autowired
    private Environment environment;

    @Autowired
    @Qualifier(READ_ONLY_DATA_SOURCE_NAME)
    private DataSource readableDataSource;

    @Autowired
    @Qualifier(UPDATABLE_DATA_SOURCE_NAME)
    private DataSource updatableDataSource;


    @Bean
    @Primary
    public RoutingDataSourceResolver multiDataSource() {
        RoutingDataSourceResolver resolver = new RoutingDataSourceResolver();

        // スイッチするデータソースを設定
        Map<Object, Object> dataSources = new HashMap<>();
        dataSources.put(READ_ONLY_DATA_SOURCE_NAME, readableDataSource);
        dataSources.put(UPDATABLE_DATA_SOURCE_NAME, updatableDataSource);

        resolver.setTargetDataSources(dataSources);
        resolver.setDefaultTargetDataSource(updatableDataSource);
        return resolver;
    }


    @Bean(READ_ONLY_DATA_SOURCE_NAME)
    public DataSource readOnlyDataSource() {
        String baseConfig = "spring.datasource.readOnly.%s";
        HikariConfig config = new HikariConfig();
        config.setDriverClassName(environment.getProperty(String.format(baseConfig, "driverClassName")));
        config.setJdbcUrl(environment.getProperty(String.format(baseConfig, "url")));
        config.setUsername(environment.getProperty(String.format(baseConfig, "username")));
        config.setPassword(environment.getProperty(String.format(baseConfig, "password")));
        HikariDataSource dataSource = new HikariDataSource(config);
        dataSource.setMinimumIdle(Integer.parseInt(environment.getProperty(String.format(baseConfig, "minIdlePoolSize"))));
        dataSource.setMaximumPoolSize(Integer.parseInt(environment.getProperty(String.format(baseConfig, "maximumPoolSize"))));
        dataSource.setIdleTimeout(Long.parseLong(environment.getProperty(String.format(baseConfig, "idleTimeout_ms"))));
        dataSource.setMaxLifetime(Long.parseLong(environment.getProperty(String.format(baseConfig, "maxLifetime_ms"))));
        return dataSource;
    }


    @Bean(UPDATABLE_DATA_SOURCE_NAME)
    @FlywayDataSource
    public DataSource updatableDataSource() {
        String baseConfig = "spring.datasource.updatable.%s";
        HikariConfig config = new HikariConfig();
        config.setDriverClassName(environment.getProperty(String.format(baseConfig, "driverClassName")));
        config.setJdbcUrl(environment.getProperty(String.format(baseConfig, "url")));
        config.setUsername(environment.getProperty(String.format(baseConfig, "username")));
        config.setPassword(environment.getProperty(String.format(baseConfig, "password")));
        HikariDataSource dataSource = new HikariDataSource(config);
        dataSource.setMinimumIdle(Integer.parseInt(environment.getProperty(String.format(baseConfig, "minIdlePoolSize"))));
        dataSource.setMaximumPoolSize(Integer.parseInt(environment.getProperty(String.format(baseConfig, "maximumPoolSize"))));
        dataSource.setIdleTimeout(Long.parseLong(environment.getProperty(String.format(baseConfig, "idleTimeout_ms"))));
        dataSource.setMaxLifetime(Long.parseLong(environment.getProperty(String.format(baseConfig, "maxLifetime_ms"))));
        return dataSource;
    }

}

setDefaultTargetDataSourceメソッドに「updatableDataSource」を突っ込んでいるので、@DataSourceのつけ忘れがあった場合、更新用のDBに接続してくれるはず(ちゃんと調べてない)

DataSourceにはHikariDataSourceを使用するようにした。

良くBasicDataSourceを使っているサンプルを見かけるけど、Webサーバを起動したままPostgreSQLを再起動すると、初回アクセスのときにDBコネクションエラーが発生する。

HikariDataSourceだと勝手にコネクションを再生成してくれて、初回アクセスでも問題なかった。

BasicDataSourceでも、testOnBorrowとかvalidationQueryを設定すれば、再接続してくれるっぽいけど・・・残念ながらうまくいったことがない・・・

あと、BasicDataSourceを使用して実際に負荷をかけてみたところ、上限無しにコネクションを生成し、「クライアント多すぎぃ」とDB怒られた・・・確かにコネクションの上限設定してなかったもんね。

上限の設定について一応調べた気がするけど、面倒くさくなった

ということがあったので、DataSourceにはHikariDataSourceを使用するようにした。(2回目)


7. コントローラにアノテーションを付与

接続先を切り替える処理が完成したので、あとはコントローラのメソッドに、@DataSourceを付与していく。

以下、雑なサンプルコード

@Controller
@RequestMapping("api/v1/foo")
public class FooController {

    private final FooService service;

    @Autowired
    public FooController(FooService service) {
        this.service= service;
    }

    // 読み取りのみの処理
    @DataSource(DataSourceType.ReadOnly)
    @RequestMapping(value = "", method = RequestMethod.GET)
    public ResponseEntity getFoo() {
        return ResponseEntity.ok(this.service.getFoo());
    }

    // 更新を含む処理
    @DataSource(DataSourceType.Updatable)
    @RequestMapping(value = "", method = RequestMethod.DELETE)
    public ResponseEntity deleteFoo() {
        this.service.deleteFoo()
        return ResponseEntity.ok(new MessageOnly("Foo Deleted"));
    }

}


おわりに

少々追加するクラスが多いものの、アノテーションを付与して、DBの接続先を切り替える機能を作成することができた。

SpringBootが設定を勝手にやってくれていたところが結構あり、色んなハマったポイントがあった。

利用人数が増加するアプリを作成するときは、一番最初にこの処理を作っておくべきだと思った。

そういえばDNSサーバの構築について書いてない・・・

今回、初めてCentOSDNSサーバを構築したので、それについてはまたいつか・・・

追記。DNSサーバの構築についてはコチラ

monakaice88.hatenablog.com

Vagrantでロードバランサ+WEBサーバ×2+DBを構築

はじめに

Webアプリやら常駐サービスを開発するときに、いつも新しく仮想サーバを作成するのだけれども、容量がでかいし、構築に時間がかかる。

「いつか使うかも」って思って、全然消さなかったりする

一緒に開発するからといって、仮想サーバを渡しても、IPアドレスが変わったりして、普段気にしない環境が変わるので面倒臭い

あと開発に関わる人全員が、環境構築ができる訳ではないし

いい加減、環境構築のハードルを下げないとな~と思い、前々から使ってみようと思っていたVagrantを使ってみた


目次


構築する環境

  • DBサーバ(PostgreSQL 9.6)
    • CPU:2コア
    • MEM:1GB
  • nginx(ロードバランサ目的)
    • CPU:1コア
    • MEM:512MB
  • Webサーバ(Tomcat 8.5)×2台
    • CPU:2コア
    • MEM:1GB
  • 全部CentOS7

Vagrantプラグインの追加

設定ファイルの転送等で共有フォルダを使用するが、共有フォルダをマウントする際にエラーが発生する。

エラーを防ぐため、以下のコマンドでプラグインを最初にインストールする。

参考サイト

qiita.com

vagrant plugin install vagrant-vbguest

ベースのbox作成

DBサーバ、WEBサーバを構築する前にCentOS7に共通な設定・更新を実施し、元になるboxを作成する。

※元になるboxを作成する前は、何回も仮想環境を作り直して、新しい仮想PCができるたびに「yum update」やら「Guest Additions」の更新が走ったりとかなり時間がかかった・・・

まず、作業ディレクトリにて「vagrant init」を実行

作成された「Vagrantfile」を以下のように編集した

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"

  config.vm.provider "virtualbox" do |vb|
    vb.memory = "512"
  end

  config.vm.provision "shell", inline: <<-SHELL
# いつものやーつ
sudo yum update -y
sudo yum install vim wget -y

# 時刻を日本にする
sudo cp -p /usr/share/zoneinfo/Japan /etc/localtime

# 時刻を自動で調整するようにする
cp /etc/chrony.conf /etc/chrony.conf_preprovision
sudo sed -i -e "s/server 0.centos.pool.ntp.org iburst/server ntp.nict.jp/g" /etc/chrony.conf
sudo sed -i -e "s/server 1.centos.pool.ntp.org iburst/server ntp1.jst.mfeed.ad.jp/g" /etc/chrony.conf
sudo sed -i -e "s/server 2.centos.pool.ntp.org iburst/server ntp2.jst.mfeed.ad.jp/g" /etc/chrony.conf
sudo sed -i -e "s/server 3.centos.pool.ntp.org iburst/server ntp3.jst.mfeed.ad.jp/g" /etc/chrony.conf
sudo chronyc -a makestep

# 空き領域をゼロ埋めするために一時ディレクトリ/tmpに適当なファイルZEROを作成。中身はキッチリゼロ埋めされる。
sudo dd if=/dev/zero of=/tmp/ZERO bs=1M
sudo rm -f /tmp/ZERO
  SHELL

end

空き領域をゼロ埋めしているのは、boxサイズを小さくするため

以下のサイトのコメントを参考にした。

qiita.com

編集が終わったら以下のコマンドを実行し仮想OSを作成する

vagrant up

vagrant up」完了後、パッケージ・boxを作成(名前はmy_base_centos7にした)

vagrant package
vagrant box add my_base_centos7 package.box

もう要らないので後片付け

vagrant destroy

エクスプローラからpackage.boxの削除する

Vagrantfileの作成

Vagrantfileを以下のようにした

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  
  config.vm.box = "my_base_centos7"

  config.vm.synced_folder "./synced", "/vagrant_data", create: true

  config.vm.define :db do |db|
    db.vm.hostname = "mt-db"
    db.vm.provision "db", type: "shell", :path => "create_db_server.sh", :privileged => false
    db.vm.network :forwarded_port, id: "ssh", guest: 22, host: 7022
    db.vm.network "forwarded_port", guest: 5432, host: 5432
    db.vm.network "private_network", ip: "192.168.33.10"
    db.vm.provider "virtualbox" do |db_vb|
      db_vb.name = "mt_db"
      db_vb.cpus = 2
      db_vb.memory = "1024"
    end
  end

  config.vm.define :proxy do |proxy|
    proxy.vm.hostname = "mt-proxy"
    proxy.vm.provision "proxy", type: "shell", :path => "create_proxy_server.sh", :privileged => false
    proxy.vm.network :forwarded_port, id: "ssh", guest: 22, host: 7122
    proxy.vm.network "forwarded_port", guest: 8888, host: 8888
    proxy.vm.network "private_network", ip: "192.168.33.11"
    proxy.vm.provider "virtualbox" do |proxy_vb|
      proxy_vb.name = "mt_proxy"
      proxy_vb.cpus = 1
      proxy_vb.memory = "512"
    end
  end

  config.vm.define :web1 do |web1|
    web1.vm.hostname = "mt-web1"
    web1.vm.provision "web1", type: "shell", :path => "create_web_server.sh", :privileged => false
    web1.vm.network :forwarded_port, id: "ssh", guest: 22, host: 7222
    web1.vm.network "private_network", ip: "192.168.33.12"
    web1.vm.provider "virtualbox" do |web1_vb|
      web1_vb.name = "mt_web1"
      web1_vb.cpus = 2
      web1_vb.memory = "1024"
    end
  end

  config.vm.define :web2 do |web2|
    web2.vm.hostname = "mt-web2"
    web2.vm.provision "web2", type: "shell", :path => "create_web_server.sh", :privileged => false
    web2.vm.network :forwarded_port, id: "ssh", guest: 22, host: 7322
    web2.vm.network "private_network", ip: "192.168.33.13"
    web2.vm.provider "virtualbox" do |web2_vb|
      web2_vb.name = "mt_web2"
      web2_vb.cpus = 2
      web2_vb.memory = "1024"
    end
  end

end

フォルダ構成はこんな感じ

C:.
│  create_db_server.sh---------DB構築スクリプト
│  create_proxy_server.sh------nginx構築スクリプト
│  create_web_server.sh--------Tomcat構築スクリプト
│  Vagrantfile
│
├─.vagrant
│
└─synced--------仮想マシンと同期するフォルダ
    ├─db
    │      sample_db.dump.bz2---------DBダンプ
    │      set_password_to_db.sql-----パスワード設定SQL
    │
    ├─proxy
    │      404.html-------------NotFound画面
    │      maintenance.html-----メンテナンス画面
    │      nginx.conf-----------nginxの設定ファイル
    │      nginx.service--------自動起動の設定ファイル
    │
    └─web
            sample.war----------配置WEBアプリ
            tomcat.service------自動起動の設定ファイル
            tomcat.sh-----------/etc/profile.dに配置

「create_db_server.sh」「create_proxy_server.sh」「create_web_server.sh」には、サーバを構築するためのコマンドが入っている。

あまり参考にならないであろうプロビジョニングスクリプト


create_db_server.sh(PostgreSQL構築)


参考

monakaice88.hatenablog.com


create_proxy_server.sh(ロードバランサnginx構築)


参考

monakaice88.hatenablog.com


create_web_server.sh(Webサーバ構築)


参考

monakaice88.hatenablog.com

終わりに

少々複雑なサーバ構成でも、他の人に渡すときはVagrantfile+αで済むのはありがたいし、変更内容は基本プロビジョニングした時のスクリプトを見れば大丈夫なのは良いですな。

ただプロビジョニングのスクリプトを作成するのには凄い時間が掛かった・・・

最初に共通で使用するboxを作成しておけば、もう少し苦労が少なかったかも

nginxでセッション維持するロードバランサを構築

はじめに

よくあるWEBサーバ+DBサーバを使用した、とあるサービスを作る事になった

そのサービスをリリース後は結構な利用人数になりそうだった

なので、負荷分散ができる環境に前もって準備をしておこうと思った

ただし管理画面があるので、とあるURL以下はセッション維持したい(AndroidiOS等向けのAPIは振り分けて、ブラウザからの管理画面へのアクセスはセッション維持する)

nginxでロードバランサ的なことができるとは知っていたけど、セッション維持がどうやるか・・・

色々調べた結果、セッション維持はnginxにモジュールを追加することで出来そうだと分かった

以下のサイトを参考に環境を構築した

参考サイト

www.vultr.com

www.kumoyanet.com


目次

ロードバランサの構成

  • CentOS 7
  • nginx-1.13.7
    • 追加モジュール:nginx-sticky-module-ng-1.2.6

コンパイル環境の準備

更新やら、コンパイルに必要なパッケージ群をインストール

yum update
yum install vim
yum groupinstall 'Development Tools'
yum install epel-release

nginxが必要としているパッケージをインストール

yum install perl perl-devel perl-ExtUtils-Embed libxslt libxslt-devel libxml2 libxml2-devel gd gd-devel GeoIP GeoIP-devel

nginxのインストール

nginxのソースコードをダウンロード・展開する

nginx: download

cd /usr/local/src
wget http://nginx.org/download/nginx-1.13.7.tar.gz && tar zxvf nginx-1.13.7.tar.gz

また、nginxのコンパイルに必要な「PCRE」、「zlib」、「OpenSSL」のソースコードをダウンロードする。

cd /usr/local/src
wget https://ftp.pcre.org/pub/pcre/pcre-8.41.tar.gz && tar xzvf pcre-8.41.tar.gz
wget https://www.zlib.net/zlib-1.2.11.tar.gz && tar xzvf zlib-1.2.11.tar.gz
wget https://www.openssl.org/source/openssl-1.1.0g.tar.gz && tar xzvf openssl-1.1.0g.tar.gz

展開し終わったら要らないので削除

rm -rf *.tar.gz

ロードバランサのモジュールのソースコードを以下のサイトのからダウンロードする

bitbucket.org

wget https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng/get/1.2.6.tar.gz -O nginx-sticky-module-ng-1.2.6.tar.gz && tar xzvf nginx-sticky-module-ng-1.2.6.tar.gz

解凍したら「nginx-goodies-nginx-sticky-module-ng-c78b7dd79d0d」というディレクトリになっていた。

あとで見直してもわかるように、ディレクトリ名を変更

mv nginx-goodies-nginx-sticky-module-ng-c78b7dd79d0d nginx-sticky-module-ng-1.2.6

manコマンドでnginxのマニュアルが見れるように以下の以下のコマンドを実行

cp /usr/local/src/nginx-1.13.7/man/nginx.8 /usr/share/man/man8
gzip /usr/share/man/man8/nginx.8

以下のコマンドでマニュアルが表示される

man nginx

nginxのディレクトリに移動し「configure」を実行

cd /usr/local/src/nginx-1.13.7
./configure --prefix=/etc/nginx \
            --sbin-path=/usr/sbin/nginx \
            --modules-path=/usr/lib64/nginx/modules \
            --conf-path=/etc/nginx/nginx.conf \
            --error-log-path=/var/log/nginx/error.log \
            --pid-path=/var/run/nginx.pid \
            --lock-path=/var/run/nginx.lock \
            --user=nginx \
            --group=nginx \
            --build=CentOS \
            --builddir=nginx-1.13.7 \
            --with-select_module \
            --with-poll_module \
            --with-threads \
            --with-file-aio \
            --with-http_ssl_module \
            --with-http_v2_module \
            --with-http_realip_module \
            --with-http_addition_module \
            --with-http_xslt_module=dynamic \
            --with-http_image_filter_module=dynamic \
            --with-http_geoip_module=dynamic \
            --with-http_sub_module \
            --with-http_dav_module \
            --with-http_flv_module \
            --with-http_mp4_module \
            --with-http_gunzip_module \
            --with-http_gzip_static_module \
            --with-http_auth_request_module \
            --with-http_random_index_module \
            --with-http_secure_link_module \
            --with-http_degradation_module \
            --with-http_slice_module \
            --with-http_stub_status_module \
            --http-log-path=/var/log/nginx/access.log \
            --http-client-body-temp-path=/var/cache/nginx/client_temp \
            --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
            --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
            --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
            --http-scgi-temp-path=/var/cache/nginx/scgi_temp \
            --with-mail=dynamic \
            --with-mail_ssl_module \
            --with-stream=dynamic \
            --with-stream_ssl_module \
            --with-stream_realip_module \
            --with-stream_geoip_module=dynamic \
            --with-stream_ssl_preread_module \
            --with-compat \
            --with-pcre=/usr/local/src/pcre-8.41 \
            --with-pcre-jit \
            --with-zlib=/usr/local/src/zlib-1.2.11 \
            --with-openssl=/usr/local/src/openssl-1.1.0g \
            --with-openssl-opt=no-nextprotoneg \
            --with-debug \
            --add-module=/usr/local/src/nginx-sticky-module-ng-1.2.6

コンパイルする

make

このときはコンパイルするとエラーが発生した

以下がエラーメッセージ

/usr/local/src/nginx-sticky-module-ng-1.2.6/ngx_http_sticky_misc.c: 関数 ‘ngx_http_sticky_misc_md5’ 内:
/usr/local/src/nginx-sticky-module-ng-1.2.6/ngx_http_sticky_misc.c:152:15: エラー: ‘MD5_DIGEST_LENGTH’ が宣言されていません (この関数内での最初の使用)
   u_char hash[MD5_DIGEST_LENGTH];
               ^
/usr/local/src/nginx-sticky-module-ng-1.2.6/ngx_http_sticky_misc.c:152:15: 備考: 未宣言の識別子は出現した各関数内で一回のみ報告されます
/usr/local/src/nginx-sticky-module-ng-1.2.6/ngx_http_sticky_misc.c:152:10: エラー: 使用されない変数 ‘hash’ です [-Werror=unused-variable]
   u_char hash[MD5_DIGEST_LENGTH];
          ^
/usr/local/src/nginx-sticky-module-ng-1.2.6/ngx_http_sticky_misc.c: 関数 ‘ngx_http_sticky_misc_hmac_md5’ 内:
/usr/local/src/nginx-sticky-module-ng-1.2.6/ngx_http_sticky_misc.c:189:15: エラー: ‘MD5_DIGEST_LENGTH’ が宣言されていません (この関数内での最初の使用)
   u_char hash[MD5_DIGEST_LENGTH];
               ^
/usr/local/src/nginx-sticky-module-ng-1.2.6/ngx_http_sticky_misc.c:190:12: エラー: ‘MD5_CBLOCK’ が宣言されていません (この関数内での最初の使用)
   u_char k[MD5_CBLOCK];
            ^
/usr/local/src/nginx-sticky-module-ng-1.2.6/ngx_http_sticky_misc.c:190:10: エラー: 使用されない変数 ‘k’ です [-Werror=unused-variable]
   u_char k[MD5_CBLOCK];
          ^
/usr/local/src/nginx-sticky-module-ng-1.2.6/ngx_http_sticky_misc.c:189:10: エラー: 使用されない変数 ‘hash’ です [-Werror=unused-variable]
   u_char hash[MD5_DIGEST_LENGTH];
          ^
cc1: all warnings being treated as errors

解決方法は、bitbucketのissuesにあった

(しばらくメンテナンスされていないっぽいけど、今後大丈夫なんだろうか・・・)

nginx-goodies / nginx-sticky-module-ng / issues / #33 - Cannot compile with nginx 1.13.4 and module version 1.2.6 - error: ‘MD5_DIGEST_LENGTH’ undeclared — Bitbucket

ファイル:ngx_http_sticky_misc.cのinclude部分に以下を追記

#ifndef MD5_DIGEST_LENGTH
#include <openssl/md5.h>
#endif
#ifndef SHA_DIGEST_LENGTH
#include <openssl/sha.h>
#endif

改めて、コンパイルとインストールを実行

make
make install

設定でモジュールがロードできるようにシンボリックリンクを作成

ln -s /usr/lib64/nginx/modules /etc/nginx/modules

以下のコマンドが実行できればOK

nginx -V

nginxのユーザを作成する

useradd --system --home /var/cache/nginx --shell /sbin/nologin --comment "nginx user" --user-group nginx

ちなみに、設定ファイルの確認は以下のコマンドで確認できる。

nginx -t

このコマンドを実行すると、「ディレクトリが作れねぇ!」みたいなエラーがでてくる

nginx: [emerg] mkdir() "/var/cache/nginx/client_temp" failed (2: No such file or directory)

このときは、「/var/cache/nginx」というディレクトリを作成する。

mkdir -p /var/cache/nginx

nginxの起動と自動起動

nginxを自動起動させるため、「/usr/lib/systemd/system/nginx.service」を作成する

vim /usr/lib/systemd/system/nginx.service

ファイルの中身はこんな感じ(参考サイトのコピペ)

[Unit]
Description=nginx - high performance web server
Documentation=https://nginx.org/en/docs/
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/var/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -c /etc/nginx/nginx.conf
ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID

[Install]
WantedBy=multi-user.target

nginxの起動と自動起動の有効化

systemctl start nginx
systemctl enable nginx

nginxの設定

管理画面のみセッションを維持したいので、ロードバランサの方式は、セッションを維持するタイプと維持しないタイプ2種類用意する

  • keep_session_backends
    • 管理画面用
  • default_backends
    • その他
upstream keep_session_backends {
    sticky;
    server 192.168.33.12:8080;
    server 192.168.33.13:8080;
}

upstream default_backends {
    server 192.168.33.12:8080;
    server 192.168.33.13:8080;
}

「/example」以下は「default_backends」を使用し、 「/example/admin」以下は「keep_session_backends」を使用する設定

location /example {
    proxy_pass http://default_backends;
}

location /example/admin {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-Port $server_port;
    proxy_redirect off;
    proxy_pass http://keep_session_backends;
}

「/example/admin」内の「proxy_set_header」は、Webアプリでリダイレクトするときに、ドメイン・ポート番号が変わらないようにしている

参考サイト

qiita.com

qiita.com

全体はこんな感じ(コメントは削除)

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    upstream keep_session_backends {
        sticky;
        server 192.168.33.12:8080;
        server 192.168.33.13:8080;
    }

    upstream default_backends {
        server 192.168.33.12:8080;
        server 192.168.33.13:8080;
    }

    server {
        listen       8888;
        server_name  localhost;
        port_in_redirect on;

        location /example {
            proxy_pass http://default_backends;
        }

        location /example/admin {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Port $server_port;
            proxy_redirect off;
            proxy_pass http://keep_session_backends;
        }

        error_page  404              /404.html;
        location = /404.html {
            root   html;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

※ポート番号は、開発環境のため、8888番を使用

終わりに

「example/admin」以外の「example」以下には、交互にアクセスし、「example/admin」以下には片方のみアクセスするのを確認できた。

ちなみに今回使用した追加モジュールの「nginx-sticky-module-ng」は、2016年08月09日以降コミットがない・・・

nginxのバージョンが上がったときに、ちゃんと動作するのか心配