ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ZooKeeper 와 Spring Cloud ZooKeeper
    DEV 2021. 9. 16. 22:53

    회사에서 사용하는 기술 중 ZooKeeper 가 무엇인지 궁금하여 공부하면서 정리했습니다.
    부족한 부분은 댓글로 남겨주세요.

    ZooKeeper 란?

    ZooKeeper는 분산 애플리케이션을 위한 분산 오픈 소스 조정 서비스입니다.

    주키퍼는 파일 시스템과 비슷한 namespace 를 제공합니다.

    파일 시스템처럼 / 부터 시작하며 주키퍼의 네임스페이스에 있는 모든 노드는 경로로 식별됩니다.

    실행

    docker compose 를 이용하여 zookeeper 를 실행합니다

    version: '3.1'
    
    services:
      zoo1:
        image: zookeeper
        restart: always
        ports:
          - "2181:2181"

    실행이 완료되면 docker exec -it {CONTAINER ID} bin/zkCli.sh 명령어로 Zookeeper CLI 에 접속할 수 있습니다.

    CLI 명령어

    처음에 접속 후 ls / 명령어를 입력하여 최상위에 어떤 노드가 있는지 확인할 수 있습니다.

    [zk: localhost:2181(CONNECTED) 0] ls /
    [services, zookeeper]

    기본적으로 serviceszookeeper 노드가 존재합니다.

    노드 생성

    create /test 명령어로 노드를 생성할 수 있습니다.

    [zk: localhost:2181(CONNECTED) 1] create /test
    Created /test
    [zk: localhost:2181(CONNECTED) 2] ls /
    [services, test, zookeeper]

    데이터 쓰기, 조회

    set [path] data 로 해당 경로에 데이터를 쓰고 get [path] 로 해당 경로의 데이터를 조회할 수 있습니다.

    [zk: localhost:2181(CONNECTED) 9] set /test a,b,c
    [zk: localhost:2181(CONNECTED) 10] get /test
    a,b,c

    get -w [path 를 이용하면 watcher 를 등록할 수 있습니다.

    해당 노드의 데이터가 변경되면 watcher 이벤트가 동작하여 변경을 알려줍니다.

    [zk: localhost:2181(CONNECTED) 19] get -w /test
    a,b,c
    [zk: localhost:2181(CONNECTED) 20] set /test a,b
    
    WATCHER::
    
    WatchedEvent state:SyncConnected type:NodeDataChanged path:/test

    노드 삭제

    delete [path] 로 노드를 삭제할 수 있습니다. 노드가 비어있지 않을 때는 삭제되지 않으며 deleteall [path] 로 하위 노드까지 한번에 삭제할 수 있습니다.

    [zk: localhost:2181(CONNECTED) 41] delete /test
    Node not empty: /test
    [zk: localhost:2181(CONNECTED) 42] deleteall /test
    [zk: localhost:2181(CONNECTED) 43] ls /
    [services, zookeeper]

    Spring Boot 에서 ZooKeeper 사용하기

    설정

    build.gradle.kts

    implementation("org.springframework.cloud:spring-cloud-starter-zookeeper-discovery") 의존성을 추가합니다.

    spring-cloud-zookeeper-discovery 하위에는 org.apache.zookeeper 가 포함되어 있습니다.

    ./gradlew dependencies --configuration compileClasspath 명령어로 gradle 의존성을 트리 구조로 확인할 수 있습니다.

    application.yml

    zookeeper 의 설정 속성을 작성합니다.

    spring:
      application:
        name: ZooKeeperTest
      cloud:
        zookeeper:
          connect-string: localhost:2181
          discovery:
            enabled: true
    logging:
      level:
        org.apache.zookeeper.ClientCnxn: WARN

    [spring.application.name](http://spring.application.name) 에 작성한 이름으로 주키퍼에 /services 하위에 노드가 생성됩니다.

    @SpringBootApplication 이 위치한 곳에 @EnableDiscoveryClient 를 추가합니다.

    @SpringBootApplication
    @EnableDiscoveryClient
    class ZookeeperStudyApplication

    DiscoveryClient

    DiscovertClient 를 사용하여 ServiceInstance 의 정보를 조회할 수 있습니다.

    @RestController
    @RequestMapping("zoo-keeper")
    class ZooKeeperController(
        private val discoveryClient: DiscoveryClient
    ) {
    
        @GetMapping
        fun getServiceInstance(): ServiceInstance? {
            val instances = discoveryClient.getInstances("ZooKeeperTest")
            if (instances.isNullOrEmpty()) {
                return null
            }
            return instances[0]
        }
    }

    테스트용 컨트롤러를 생성하여 ServiceInstance 를 반환하도록 했습니다.

    {
        "serviceId": "ZooKeeperTest",
        "host": "192.168.0.44",
        "port": 8080,
        "secure": false,
        "uri": "http://192.168.0.44:8080",
        "metadata": {
            "instance_status": "UP"
        },
        "serviceInstance": {
            "name": "ZooKeeperTest",
            "id": "a0aaedf2-6c65-4c7b-9213-a9cfaa138809",
            "address": "192.168.0.44",
            "port": 8080,
            "sslPort": null,
            "payload": {
                "@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance",
                "id": "ZooKeeperTest",
                "name": "ZooKeeperTest",
                "metadata": {
                "instance_status": "UP"
                }
            },
            "registrationTimeUTC": 1631535120395,
            "serviceType": "DYNAMIC",
            "uriSpec": {
                "parts": [
                    {
                        "value": "scheme",
                        "variable": true
                    },
                    {
                        "value": "://",
                        "variable": false
                    },
                    {
                        "value": "address",
                        "variable": true
                    },
                    {
                        "value": ":",
                        "variable": false
                    },
                    {
                        "value": "port",
                        "variable": true
                    }
                ]
            },
            "enabled": true
        },
        "instanceId": "a0aaedf2-6c65-4c7b-9213-a9cfaa138809",
        "scheme": null
    }

    서비스의 이름, uri, 포트 등의 정보가 조회됩니다.

    ServiceInstance 등록

    ZookeeperServiceRegistry 로 새로운 ServiceInstance 를 추가할 수 있습니다.

    @PostMapping
    fun register() {
        ServiceInstanceRegistration.builder().defaultUriSpec().address("/test/url").port(9000)
            .name("/test/anotherService").build().let {
            serviceRegistry.register(it)
        }
    }

    post 호출을 하여 새로운 ServiceInstance 를 추가해보겠습니다.

    curl -X POST 'http://localhost:8080/zoo-keeper'

    호출 후 주키퍼 CLI 에서 /services 하위 경로를 확인해보면 등록할 때 작성한 name 으로 노드가 생성되어 있습니다.

    [zk: localhost:2181(CONNECTED) 64] ls /services/test/anotherService
    [630bca1f-44d0-4207-8ccc-9b7dd32f875a]

    instanceId 로 조회해 보면 입력한 이름, address, 포트가 잘 등록된 것을 확인할 수 있습니다.

    [zk: localhost:2181(CONNECTED) 66] get /services/test/anotherService/630bca1f-44d0-4207-8ccc-9b7dd32f875a
    {
        "name": "/test/anotherService",
        "id": "630bca1f-44d0-4207-8ccc-9b7dd32f875a",
        "address": "/test/url",
        "port": 9000,
        "sslPort": null,
        "payload": null,
        "registrationTimeUTC": 1631536229270,
        "serviceType": "DYNAMIC",
        "uriSpec": {
            "parts": [
                {
                    "value": "scheme",
                    "variable": true
                },
                {
                    "value": "://",
                    "variable": false
                },
                {
                    "value": "address",
                    "variable": true
                },
                {
                    "value": ":",
                    "variable": false
                },
                {
                    "value": "port",
                    "variable": true
                }
            ]
        }
    }

    한번더 post 호출을 하여 등록하게 되면

    [zk: localhost:2181(CONNECTED) 68] ls /services/test/anotherService
    [3d2bacec-b99d-4fdf-9b2f-c4f3adac3e08, 630bca1f-44d0-4207-8ccc-9b7dd32f875a]

    같은 노드 안에 2개의 serviceId가 생성된 것을 확인할 수 있습니다.

    @GetMapping("anotherService")
    fun getAnotherServiceInstance(): ServiceInstance? {
        val instances = discoveryClient.getInstances("/test/anotherService")
        if (instances.isNullOrEmpty()) {
            return null
        }
        return instances[0]
    }

    anotherService 의 ServiceInstance 를 조회하는 함수를 생성하여 확인할 수 있습니다.

    LoadBalancerClient

    Spring Cloud 에서 제공하는 LoadBalancerClient 이용하여 로드 밸런서 기능을 사용할 수 있습니다.

    class ZooKeeperController(
        ...
        private val loadBalancer: LoadBalancerClient,
    ){
        ...
    
        @GetMapping("lb")
        fun getServiceInstanceUsingLB(): ServiceInstance {
            return loadBalancer.choose("ZooKeeperTest")
        }
    }

    LoadBalanverClient 에서 choose 메소드를 이용하여 가져올 서비스 Id 를 입력합니다.

    서버를 다시 실행하고 http://localhost:8080/zoo-keeper/lb 로 접속하면 처음에 접속했던 것 과 동일하게 ServiceInstance 정보를 확인할 수 있습니다.

    이때 같은 서버를 포트만 9090으로 변경하여 하나 더 실행시켜보겠습니다.

    포트를 변경하여 실행할 때는 인텔리제이 기준으로 VM options-Dserver.port=9090 를 넣어주면 됩니다.

    주키퍼 cli 에서 확인해보면 같은 노드에 2개의 서비스가 있습니다.

    [zk: localhost:2181(CONNECTED) 75] ls /services/ZooKeeperTest
    [af31d159-fd04-4042-93ea-42a811fa70b5, c4bcc464-1a39-46cf-9d53-e478318470be]

    다시 위의 주소로 접속을 반복해서 해보면 8080 포트와 9090 포트로 번갈아가며 표시됩니다.

    Untitled

    Untitled

    로드 밸런서 타입이 기본적으로 라운드 로빈 방식이기 때문에 번갈아가며 선택됩니다.

    DependencyWatcherListener

    spring:
      cloud:
        zookeeper:
          dependencies:
            dependencyOne:
              path: ZooKeeperTest

    dependencies 설정을 추가하여 등록한 dependency 의 변경을 감시하는 Watcher 를 생성할 수 있습니다.

    @Component
    class ZookeeperListener: DependencyWatcherListener {
        companion object{
            val logger = LoggerFactory.getLogger(this::class.java)
      }
    
        override fun stateChanged(dependencyName: String?, newState: DependencyState?){
            logger.info(dependencyName)
            logger.info(newState.toString())
        }
    }

    DependencyWatcherListener 를 구현하는 ZookeeperListener 를 생성하고 stateChanged 메소드를 override 하여 구현하여 dependency 의 state 가 변경되면 dependencyName 과 새로운 state 를 로그로 찍어주도록 했습니다.

    8080 으로 서버를 실행한 후 9090으로 하나를 더 실행해보면

    INFO 30663 --- [NotifyService-0] .w.DependencyStateChangeListenerRegistry : Service cache state change for '/ZooKeeperTest' instances, current service state: CONNECTED
    INFO 30663 --- [NotifyService-0] c.m.z.ZookeeperListener$Companion        : /ZooKeeperTest
    INFO 30663 --- [NotifyService-0] c.m.z.ZookeeperListener$Companion        : CONNECTED

    설정에서 추가한 path 의 인스턴스의 state 가 변경되었을 때 로그가 출력됩니다.


    예시로 사용한 소스는 이곳에 있습니다.
    https://github.com/sinna94/zookeeper-study

    참고

    https://zookeeper.apache.org/

    https://www.baeldung.com/spring-cloud-zookeeper

    https://github.com/spring-cloud/spring-cloud-zookeeper

    반응형

    댓글

Designed by Tistory.