SpringBootで接続先のデータベースを動的に切り替える
はじめに
とあるサービスを作成することになり、想定する利用人数をもとに、負荷分散について調べていた。
現状大丈夫そうだなと思っていても、今後利用人数が増えることを考えると、DBの負荷分散はどうしたら良いのか分からなかった。
負荷分散について考える前までは、レプリケーションやら、AWSのリードレプリカの概要・説明が出てくるけど、アプリケーションの変更が必要なのか良く分からなかった。
調べてみると、やっぱりアプリケーションの改造をやるか、PostgreSQLの場合はpgpoolのようなミドルウェアが必要っぽい。
AWS上の環境をあまり増やしたくないので、SpringBootで作成したアプリケーションに、DBの接続先を切り替える機能を作ることにした。
目次
- はじめに
- 参考サイト
- 環境
- 作業の概要
- 1. コンテキストを保持するクラスを作成
- 2. コンテキストより、使用するBean名を返すクラスの作成
- 3. 接続先を指定するアノテーションを作成
- 4. コンテキストを切り替えるAspectを作成
- 5. 読み取り用と更新用とそれぞれのDB接続設定を追加
- 6. Beanを作成
- 7. コントローラにアノテーションを付与
- おわりに
参考サイト
以下のサイトを参考にした。
環境
- Java 1.8
- Spring Boot 1.5.4.RELEASE
- PostgreSQL 9.6
AWS上で想定している将来の環境は以下の感じ。
ということで、開発環境にDNSサーバ・レプリケーションを構成済みのPostgreSQL2台を構築した。
DBのロードバランスは、DNSサーバにお任せすることにした。
作業の概要
いろいろやることがあったので、ざっくり以下にまとめ
- コンテキストを保持するクラスを作成
- コンテキストより、使用するBean名を返すクラスの作成
- 接続先を指定するアノテーションを作成
- コンテキストを切り替えるAspectを作成
- 読み取り用と更新用とそれぞれのDB接続設定を追加
- Beanを作成
- コントローラにアノテーションを付与
サービスにアノテーションを付与するようにしたかったけれども、サービスに付与するとテストが通らなった。
コントローラに付与すると問題なく動作し、また既に色々処理が出来てしまっていたので、今回はコントローラにアノテーションを付与ようにした。
(コントローラのメソッドの中で、「読み込み用」のサービスメソッドの次に「更新用」サービスメソッドを呼んでいるのが原因。"このコネクションじゃ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サーバの構築について書いてない・・・
今回、初めてCentOSでDNSサーバを構築したので、それについてはまたいつか・・・
追記。DNSサーバの構築についてはコチラ
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にプラグインの追加
設定ファイルの転送等で共有フォルダを使用するが、共有フォルダをマウントする際にエラーが発生する。
エラーを防ぐため、以下のコマンドでプラグインを最初にインストールする。
参考サイト
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サイズを小さくするため
以下のサイトのコメントを参考にした。
編集が終わったら以下のコマンドを実行し仮想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構築)
参考
create_proxy_server.sh(ロードバランサnginx構築)
参考
create_web_server.sh(Webサーバ構築)
参考
終わりに
少々複雑なサーバ構成でも、他の人に渡すときはVagrantfile+αで済むのはありがたいし、変更内容は基本プロビジョニングした時のスクリプトを見れば大丈夫なのは良いですな。
ただプロビジョニングのスクリプトを作成するのには凄い時間が掛かった・・・
最初に共通で使用するboxを作成しておけば、もう少し苦労が少なかったかも
nginxでセッション維持するロードバランサを構築
はじめに
よくあるWEBサーバ+DBサーバを使用した、とあるサービスを作る事になった
そのサービスをリリース後は結構な利用人数になりそうだった
なので、負荷分散ができる環境に前もって準備をしておこうと思った
ただし管理画面があるので、とあるURL以下はセッション維持したい(Android、iOS等向けのAPIは振り分けて、ブラウザからの管理画面へのアクセスはセッション維持する)
nginxでロードバランサ的なことができるとは知っていたけど、セッション維持がどうやるか・・・
色々調べた結果、セッション維持はnginxにモジュールを追加することで出来そうだと分かった
以下のサイトを参考に環境を構築した
参考サイト
目次
ロードバランサの構成
- 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のソースコードをダウンロード・展開する
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
ロードバランサのモジュールのソースコードを以下のサイトのからダウンロードする
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にあった
(しばらくメンテナンスされていないっぽいけど、今後大丈夫なんだろうか・・・)
ファイル: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アプリでリダイレクトするときに、ドメイン・ポート番号が変わらないようにしている
参考サイト
全体はこんな感じ(コメントは削除)
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のバージョンが上がったときに、ちゃんと動作するのか心配