Walk, Don't Run

若手エンジニアが日々学んだことをさらけ出すブログです

Spring CloudのRibbonによるロードバランシング

Spring Cloudとは

マイクロサービスでよく見られる共通パターンを迅速に構築するためのツール群。

  • Distributed/versioned configuration (Spring Cloud Config)
  • Service registration and discovery (Eureka)
  • Routing (Zuul)
  • Service-to-service calls (Eureka)
  • Load balancing (Ribbon)
  • Circuit Breakers (Hystrix)
  • Global locks (Spring Cloud Cluster?)
  • Leadership election and cluster state (Spring Cloud Cluster?)
  • Distributed messaging (Spring Cloud Bus?)

括弧内には書いたのは対応するであろうライブラリ。

Ribbonとは

クライアントサイドロードバランサー。その名の通り、呼び出し元で呼び出し先のサービスをどのようにロードバランシングするかを定義する。 ドキュメントとしては以下のあたりを参考に。

実装例(kotlin)

getting startedをほぼそのままの実装例。以下の2つのサービスが登場人物。

  • say-hello-service
    • GET / -> 「Hi!」と返すだけ。ヘルスチェックのためのエンドポイントっぽい。
    • GET /greeting -> 挨拶を返す(挨拶の種類はランダム)
  • user-service
    • GET /hi?name=xxx -> 受け取ったnameに対して挨拶を返すサービス。どんな挨拶をするかはsay-hello-serviceを呼び出して決める。

user-serviceがクライアントであり、Ribbonの実装はこちらに記述する。say-hello-serviceが複数立ち上がっている状態でuser-serviceを経由して呼び出し、ロードバランシングされることが確認できればOK。

コードは https://github.com/suzukimit/spring-cloud-demo にあります。

say-hello-serviceの実装

application.yml

spring:
  application:
    name: say-hello

server:
  port: 8090

アプリ名に say-hello を指定。

build.gradle

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

とりあえずspring-boot-starter-webがあればOK。

Controller

@RestController
class SayHelloController {
    companion object {
        private val log = LoggerFactory.getLogger(SayHelloController::class.java)
    }

    @Value("\${server.port}")
    lateinit var port: String

    @RequestMapping(value = "/greeting")
    fun greet(): String {
        log.info("Access /greeting")

        val greetings = listOf("Hi there", "Greetings", "Salutations")
        val rand = Random()

        val randomNum = rand.nextInt(greetings.size)
        return greetings[randomNum] + "(from $port)"
    }

    @RequestMapping(value = "/")
    fun home(): String {
        log.info("Access /")
        return "Hi!"
    }
}

/greeting を呼び出すことで適当な挨拶を返す。ルート( / )はクライアントサイドからのpingに応答するヘルチェック用のもので、アクセスされた際にログに出力。

user-serviceの実装

application.yml

spring:
  application:
    name: user

server:
  port: 8888

say-hello:
  ribbon:
    eureka:
      enabled: false
    listOfServers: localhost:8090,localhost:9092,localhost:9999
    ServerListRefreshInterval: 15000

ロードバランシングの対象となるサービスのアプリ名(say-hello)に対してribbonの設定を記述する。

ここではeurekaを使用をOFFにしている。そのため listOfServers で静的にリクエスト先を指定している(eurekaを使う場合は不要と思われる)。

build.gradle

dependencies {
    implementation("org.springframework.cloud:spring-cloud-starter-netflix-ribbon")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:Finchley.SR2")
    }
}

spring-boot-starter-webに加え、spring-cloud-starter-netflix-ribbonが必要。

Configuration

@Configuration
class RibbonConfiguration {

    @Bean
    fun ribbonPing(): IPing {
        return PingUrl()
    }

    @Bean
    fun ribbonRule(): IRule {
        return AvailabilityFilteringRule()
    }
}

ping(ヘルスチェック)のurlや、ロードバランシングルールの設定など。

Controller

@RestController
@RibbonClient(name = "say-hello", configuration = [RibbonConfiguration::class])
class UserController {
    @LoadBalanced
    @Bean
    fun restTemplate() = RestTemplate()

    @Autowired
    lateinit var restTemplate: RestTemplate

    @RequestMapping("/hi")
    fun hi(@RequestParam(value = "name", defaultValue = "Artaban") name: String): String {
        val greeting = this.restTemplate.getForObject("http://say-hello/greeting", String::class.java)
        return String.format("%s, %s!", greeting, name)
    }
}

RestTemplateに @LoadBalanced が付与されており、これによって http://say-hello/greeting のようにアプリケーション名でリクエストを送ることができる。

動作確認

say-hello-service は全部で三つ立ち上げる。ポートは何も指定しないと8090になるので、コマンドライン引数なり環境変数なりで9092, 9999ポートを指定して起動する。

あとは、curlでuser-serviceにリクエストを投げ、毎回ポート番号が異なることを確認できればOK。

$ curl http://localhost:8888/hi
Salutations(from 9092), Artaban!