asoview! developers blog

ワクワクをすべての人に。

Aurora のカスタムエンドポイントを使ったサービス可用性の改善

こんにちは。アソビューで SRE チームを担当している秋元です。

アソビューのシステムやサービス運用をする上での取り組みを紹介していきたいと思います。
今回は、Aurora のカスタムエンドポイントを使ってサービス可用性を改善している話を書きたいと思います。

アソビューではデータベースに AWS の Aurora を使用しているんですが、Aurora のカスタムエンドポイントという機能を使って一部のサービスレベルの高いアプリケーションが他のアプリケーションの影響を受けないようにしています。

Aurora のカスタムエンドポイントはそれほど新しいネタではありませんが、アソビュー内の取り組みの紹介ということでご容赦いただければと思います。

前提

他のスタートアップのサービスでもよくあるケースかとは思いますが、アソビューのシステムでは複数のアプリケーションから参照されている Aurora クラスターがあり、そこの負荷が高まると参照している全てのアプリケーションのレスポンスに影響が出てしまう、という問題がありました。
またこの Aurora クラスターへは、アプリケーションからのアクセス以外にも、集計、分析用の重い SQL が実行されているなど、様々な用途に使用されていました。
そのため、集計、分析用の SQL やサービスレベルのそれほど高くないアプリケーションによる負荷の増加が、サービスレベルの高いアプリケーションに影響を与えてしまうという状況でした。
本来であれば、少なくともスキーマなどの単位でアプリケーションごとデータが分割されているのがいいかと思いますが、テーブルレベルで共有されているものもあり、データ移行してクラスターを分割、というのも難しい状況でした。

改善の対応

この状況を改善するために、Aurora のカスタムエンドポイントを利用し、ロールごとにアクセスするリードインスタンスを分割しました。
これにより、サービスレベルの高いアプリケーションが他のアプリケーションの影響を受けないようにしたり、データ集計などの重い SQL の実行がアプリケーションに影響を与えないようにすることができました。

Aurora カスタムエンドポイント

具体的にどういった対応をしているかをお話しする前に、Aurora カスタムエンドポイントとはどういったものかをご説明します。

Aurora にはデフォルトで writer と reader のエンドポイントがあり、writer のエンドポイントに接続すると、Aurora クラスターのマスターインスタンスへ、reader のエンドポイントに接続するとリードインスタンスのどれかへアクセスします。
このデフォルトのエンドポイントに加えカスタムエンドポイントを設定することができ、接続するリードインスタンスを柔軟に設定することができます。

接続する先のインスタンスは、ブラックリストかホワイトリストで指定できます。 AWS CLI で作成する場合、ブラックリストは --excluded-members オプションで、ホワイトリストは --static-members オプションで指定できます。

docs.aws.amazon.com

カスタムエンドポイントには

  • READER
  • ANY

の 2 種類があります。
READER のエンドポイントはマスターインスタンスが接続先の対象から除かれますが、ANY の場合は除かれません。

どのように使用しているか?

現在 3 つのカスタムエンドポイントを作成して使用しています。

  • アプリケーション用(サービスレベル高い)
  • アプリケーション用(その他)
  • 集計、分析用

f:id:akmtr:20191105143427j:plain
Aurora カスタムエンドポイントの構成

カスタムエンドポイントの設定

それぞれの設定は下記の通りです。

エンドポイント インスタンスの指定 エンドポイントのタイプ
アプリケーション用(サービスレベル高い) static-members ANY
アプリケーション用(その他) excluded-members READER
集計、分析用 static-members READER

設定の意図は下記の通りです。

  • 「アプリケーション用(その他)」のインスタンスをスケールアウトさせるケースが多いので、インスタンスの指定を excluded-members で行なっています。
  • 「アプリケーション用(サービスレベル高い)」のインスタンスがマスターインスタンスになってもアクセスできるように、エンドポイントのタイプは ANY で指定しています。
  • 「集計、分析用」のアクセスがマスターインスタンスに影響を与えないよう、エンドポイントのタイプは READER に指定しています。

フェールオーバーの設定

Aurora クラスターのインスタンスは、フェールオーバーが発生した際にどれがマスターインスタンスになるかの優先度を設定することができます。
カスタムエンドポイントを利用してロールごとにアクセスするインスタンスを設定している場合、優先度を適切に設定しておかないとマスターインスタンスへの昇格で、本来負荷をあまりかけたくないインスタンスに意図せず負荷がかかってしまいます。

弊社では優先度を下記のように設定しています。

  • 「アプリケーション用(その他)」のインスタンスの優先度を小さく、「アプリケーション用(サービスレベル高い)」のインスタンスの優先度を大きくし、「アプリケーション用(その他)」のインスタンスに基本的にフェールオーバーするように設定しています。 (優先度の設定値が小さい方が優先的にマスターインスタンスに昇格します。)
  • 「アプリケーション用(その他)」のエンドポイントのタイプは READER にし、フェールオーバーが発生してもマスターインスタンスに余計な負荷がかからないようにしています。

まとめ

今回は、Aurora のカスタムエンドポイントの機能を利用して、アクセスするインスタンスを分散させる対応をご紹介しました。

  • Aurora のカスタムエンドポイントの機能を利用することで、アクセスするインスタンスを分散することができます。
  • 複数のアプリケーション間でテーブルレベルでデータが共有されている場合でも、低コストで分散することが可能です。
  • カスタムエンドポイントの設定やインスタンスのフェールオーバーの設定を適切に行わないと、運用を進めるうちに意図しない構成になってしまうので注意が必要です。

こちらは Aurora のシングルマスターのクラスターを想定して記載しています。
書き込みの負荷を分散させたい場合は、 Aurora のマルチマスター、Aurora クラスター間のレプリケーションなど、別途対応を検討する必要がありますので、ご注意ください。

HandlerInterceptorAdapterで共通処理を行う

こんにちは、@seitです。 Spring Bootで、各Controllerの共通処理を実装する機会があったので、備忘録です。

サンプルはこちらからどうぞ。

やりたいこと

HTTPリクエストに対して、どこにアクセスされたのかをログに出したい。
全てのリクエストハンドラ(Controller)に処理を入れるのは、実装の工数や保守性の観点でやりたくないので共通化したい。

利用するAPI

今回はHandlerInterceptorAdapterを使います。
Spring Bootで共通処理を挟み込む方法はいくつかあります。

名前 概要
AdviceController Controller専用の特殊なメソッド(initBinderやExceptionHandler、ModelAttribute)を複数のControllerで共有することが可能
サーブレットフィルター DispatcherServletの呼び出し前後に共通処理を挟み込むことが可能
HandlerInterceptor DispatcherServletとリクエストハンドラとの間に共通処理を挟み込むことが可能。Controllerに対してだけ共通処理を実行したい場合に利用
AOP アスペクト指向プログラミング

今回は、HTTPリクエストでハンドラが呼ばれる前に、どのコールされたハンドラを記録するログを出力したいと思います。
この場合、以下の2つの理由から、HandlerInterceptorが適していると思いました。

  • リクエストを受け取るControllerに対してだけ共通処理を実装すればよい

  • ハンドラの関数名をログに出力したいので、DispatcherServletとControllerの間に処理を挟みたい

  • initBinderやModelAttributeはログを出すだけという目的と用途が合致しない

  • AOPはそこまでする必要はなさそう

実装

まずは、HandlerInterceptorを継承したクラスを作成し、Overrideしたメソッドに共通にしたい処理を実装します。
今回は、HTTPリクエストの内容をいくつか取得してログに出力してみます。

public class LoggingInterceptor extends HandlerInterceptorAdapter {

    Logger logger = org.slf4j.LoggerFactory.getLogger(getClass().getPackage().getName());

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String requestUri = request.getRequestURI();
        String requestMethod= request.getMethod();
        String referer = request.getHeader("Referer");
        HandlerMethod hm = (HandlerMethod) handler;
        String handlerClassName = hm.getMethod().getDeclaringClass().getSimpleName();
        String handlerMethodName = hm.getMethod().getName();

        String logInfo = new StringBuffer()
                .append(requestUri)
                .append(", method ")
                .append(requestMethod)
                .append(", from ")
                .append(referer)
                .append(", handlerClassName")
                .append(handlerClassName)
                .append(", handlerMethodName")
                .append(handlerMethodName).toString();

        logger.info(logInfo);

        return true;
    }
}

preHandleメソッドは、Controllerが呼ばれる前に実行されます。
Controllerの後に実行したい場合は、postHandleメソッドをOverrideします。

最後に、WebMvcConfigurerAdapterのaddInterceptorsメソッドの中で、
作成したLoggingInterceptorを登録します。

@Configuration
public class SampleConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/**");
    }

}

この時、addPathPatternsでLoggingInterceptorを有効にしたいURIのパターンを指定することができます。
ここでは"/"以下全てのURIへのアクセスに対して、ログを出力するように設定しています。

2019-09-29 13:54:36.347  INFO 21072 --- [nio-8080-exec-1] com.example.HandlerInterceptordemo       : /, method GET, from null, handlerClassNameSampleController, handlerMethodNametop
2019-09-29 13:54:36.353  INFO 21072 --- [nio-8080-exec-1] com.example.HandlerInterceptordemo       : これはControllerのログです。

以上です。

www.wantedly.com

Figmaを使ったエンジニアとデザイナーの協業について 〜WEB DB PRESSに寄稿しました〜

こんにちは。アソビュー井上(@ashimon83)です。 最近のマイブームはスマブラです。ネスしか使いません。 弱小で全然VIPになれません。

さて、今回ご縁がありまして WEB DB PRESS Vol.113 にて 「FigmaによるUIデザイン デザイナーとエンジニアがオンラインで協業できる!」 というタイトルの記事に弊社エンジニア井上(私)とデザイナー山口で寄稿いたしました。

デザインアンドライフの池田様、クックパッドの藤井様との共著になります。

Figmaは最近急速に使われ始めている印象のあるコラボレーティブなUIデザインツールです。 弊社でも、あるWebアプリ開発PJでこのツールを活用して仕様検討〜デザイン〜実装を効率化することができました。

デザインツールという体を取っていますが、これまでのものとは根本的に違います。 具体的にはどういう点かというと、、、、続きは雑誌で!

gihyo.jp

ちなみに弊社ではWebアプリなどのデザイン以外にも色々な使い方をしています。

  • キックオフなどの投影資料としてパワポやスライド代わりに活用
  • ブレストのホワイトボード代わり。(postitをアプリで画像として取り込んでFigmaに放り込んで分類をしました)オンラインブレストの可能性を感じますね
    f:id:masino83:20191022183411p:plain
    Figmaでブレスト。10人以上で付箋の分類を同時にわちゃわちゃやってます。(一部付箋で遊んでる人がいますねw)

ぜひ書店で見かけたらお手にとって見てください!

以上です!

Spring Boot + Logbackでログを出力する

こんにちは、@seitです。 今回は、社内のWebシステムでログ出力周りをいじることがあったので、その時の備忘録です。

サンプルアプリ

やりたいこと

Spring Bootで、ログの出力先(標準出力、ファイル出力)を細かく切り分けたい。
・全てのログを標準出力とファイルに出力する
・一部のログは別のファイルにも切り出して出力する
・ログファイルはローテーションする
・ローテーションルールを指定する

Log出力で使用するライブラリ

Logbak+SLF4J

最初、過去に使ったことのあるlog4jを考えていましたが、公式によると"Logback は log4j プロジェクトの後継プロジェクトです。log4j の創始者であるCekiGülcü によって設計された"とのことなので、今回はLogback+SLF4Jにしました。

実装

Logbackは"spring-boot-starter-web"に含まれるため、Spring bootを利用している場合は新たに依存性を追加する必要はありません。
Logbackの設定ファイルである"logback-spring.xml"を"resources"フォルダの下に作成します。
今回は以下のようにします。

logback-spring.xml

<configuration>

    <!-- 標準出力用設定 --> ・・・① 
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>UTF-8</charset>
            <pattern>%d{yyyy/MM/dd HH:mm:ss} %-5level [%thread] - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- ファイルA出力用設定 --> ・・・②
    <appender name="FILE_A" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 出力先ファイルパス -->
        <file>/<任意のパス>/alog.log</file>

        <!-- ログのローテーション設定 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <!-- 世代管理されるファイルの命名パターン -->
            <fileNamePattern>/<任意のパス>/alog.log.%i</fileNamePattern>    <!-- %iはインデックス。minIndexからMaxIndexまでの連番 -->
            <minIndex>1</minIndex>
            <maxIndex>7</maxIndex>      <!-- 最大7ファイルまで世代管理 -->
        </rollingPolicy>

        <!-- ローテーションのトリガ。10MBに達した時点でローテーション。 -->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>10MB</maxFileSize>
        </triggeringPolicy>

        <encoder>
            <charset>UTF-8</charset>
            <pattern>%d{yyyy/MM/dd HH:mm:ss} %-5level [%thread] - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- ファイルB出力用設定 --> ・・・③
    <appender name="FILE_B" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/<任意のパス>/blog.log</file>
        
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/<任意のパス>/blog.log.%d{yyyy-MM-dd}</fileNamePattern>    
            <!-- %d{yyyy-MM-dd}の設定によって、自動でローテーションがトリガされる。日まで指定すると、毎日ローテーション。 -->

            <!-- 最大30日間保存 -->
            <maxHistory>30</maxHistory>

        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy/MM/dd HH:mm:ss} %-5level [%thread] - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- rootロガーを継承したサブロガー。rootロガーの設定を引き継ぎつつ、追加でファイルBにもログを出す --> ・・・④
    <logger name="com.example" level="INFO">
        <appender-ref ref="FILE_B" />
    </logger>

    <!-- rootロガー。ログは原則ファイルAと標準出力に出力する -->  ・・・⑤
    <root level="INFO">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE_A" />
    </root>
</configuration>

⑤で、rootロガーは標準出力用のappender①と、ファイル出力用のappender②を参照しているため、rootロガーを使ったログは標準出力とファイルAに出力されます。
④の"com.piyo"ロガーはファイルBに出力するロガーを参照しているため、"com.piyo"ロガーを使うと、ファイルBに出力されます。
ここで重要なのは、ロガーは継承関係を持つことができるということです。
で作成したロガーは全てで定義したrootロガーを継承するため、"com.piyo"ロガーを使うと標準出力とファイルAにも同時に出力されます。
(タグを使用しない場合はデフォルトのrootロガーの設定が継承されます。)
ロガーは、"com.piyo.hoge.fuga..."のように、ロガーの名称を"."で繋ぐことで設定を継承させていくことができます。

root

↑(継承)

com.example

↑(継承)

com.example.hoge

↑(継承)

com.exampleexample.hoge.fuga

この例だと、com.piyo.hoge.fugaロガーを使用する場合、com.piyoの設定を引き継ぐため、ログがファイルBにも出力されることになります。
このloggerタグのname属性にパッケージ名(今回はcom.piyo)を指定しておくことで、com.piyo以下のパっケージのクラスで継承元のロガーを利用することができたりします(以下で説明)。

ログ出力コード

ログを出力するには、以下のように実装します。

com.example.logback.demo;

import org.slf4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class SampleController {

    Logger loggerA = org.slf4j.LoggerFactory.getLogger("com.hoge"); ・・・①
    Logger loggerB = org.slf4j.LoggerFactory.getLogger(getClass().getPackage().getName()); ・・・②

    @GetMapping("/")       // URLのパスの指定
    private String top() { // リクエストを受け付けるメソッド

        loggerA.info("これはrootロガーのみ継承されているためFILE_Aのみに出力されます。");
        loggerB.info("これはcom.exampleロガーを継承しているのでFILE_AとFILE_Bに出力されます。");

        return "/index";
    }
}

getLoggerの引数に、取得するロガー名称を指定します。
①ではrootロガーのみ継承されることになるため、ファイルBには出力されず、標準出力とファイルAにのみ出力されます。
②では"com.example"ロガーを継承した"com.example.logback.demo.SampleController"ロガーが生成され、ログがファイルBにも出力されます。

以上です。

www.wantedly.com

asoview!のWebパフォーマンス改善の取り組みについて

こんにちは。アソビュー株式会社でフロントエンドのテックリードをしている井上です。 先週会社の同僚と行ったボルダリングでダメージを受けた手にむち打ち、今日も狩りに出るためコントローラーを握りしめる日々です。 今回Webアプリケーションの表示速度高速化について書きたいと思います。

最近は特に日経電子版やdev.toなどの先端事例が話題になったり、SEO観点での重要性の高まりやPWA、AMPなど高速化にも関連する新しい技術や手法などWebサイトのパフォーマンスは注目度が高いトピックだと思います。

弊社でもUXやSEOの重要な要素として表示速度については目標値を設けて取り組んで行こうとしています。 直近数ヶ月でもasoview!のサイトを対象にいくつか取り組みをしたのでその内容について軽く触れられればと思います。

続きを読む