My-Books-projects / etc

0 stars 1 forks source link

Service Discovery 구축 #111

Open minsu11 opened 7 months ago

minsu11 commented 7 months ago

Resource 무중단 배포

Front 무중단배포

minsu11 commented 7 months ago

Load Balancer


Scale up Scale out
확장성 확장에 한계 존재 지속적 확장이 가능
서버 비용 성능 증가에 따른 비용이 큼 비교적 저렴한 서버를 사용해 비용 부담이 적음

장점


요청 과정


Image

  1. gateway에서 요청이 들어오면 eureka서버에 요청을 보냄

    • 요청:
      • eureka server에 등록된 resource serverhealth상태
      • gateway는 30초마다 eureka server에 자동으로 요청을 보냄
  2. eureka serverapi server의 상태 데이터를 gateway에 응답

    • 이 때 gateway는 응답 받은 데이터를 캐싱

    ![[Pasted image 20240219203256.png]]

    • up 상태의 서버만 gateway에 응답함.

    • down 상태인 서버는 gateway가 요청을 했을 때 응답 데이터로 안넘어감

    • gateway는 다음 요청을 보낼 때까지 캐싱 된 정보로 api server에 요청을 보냄

  3. gatewayup인 상태를 확인 한 후 api server에 요청을 round robin방식으로 요청 보냄

  4. resource서버는 30초마다 eureka서버에 heart beat(심박수) 신호를 보냄

    • eureka서버는 90초 마다 api server에서 heart beat가 들어오는 지 확인
    • 90초 안에 신호가 오지 않았으면 eureka서버는 신호가 오지 않은 resourse서버를 down 상태로 변경
    • 이런 경우엔 실제 resourse서버는 살아 있지만, eureka서버에는 down상태여서 gatewaydown상태로 표시된 서버로 요청을 보내지 않음

무 중단 배포 과정


resourse2 서버를 먼저 배포

Image

Image

  1. gateway캐싱하여 round robin방식으로 요청을 보내는 지 확인

    • 요청을 보내는 지 확인하는 이유:
      • gateway가 30초 마다 캐싱하는 시간이 있으므로 resource2서버가 eureka서버에 up상태여도 캐싱이 되지 않았으면 resource2서버에 요청을 보내지 않음
      • 이 경우에 up 상태가 됐다고 바로 resource1서버를 종료 시켜버리면 client가 볼 땐 서버가 연결이 안됨
    • 수동일 땐 직접 입력을 해서 서버가 캐싱 유무를 측정할 수 있지만, CI/CD단계에서는 불가능 하므로 이 때도 gateway캐싱시간을 기다림
  2. gateway캐싱이 되면 resource1eureka서버에 본인의 health 상태를 down으로 만드는 요청을 보냄

  3. eureka서버는 요청이 오면 resource1서버의 health 상태를 down으로 표시

    • 주의할 점은 위에 resource2 배포 과정을 설명 할 때 했으므로 넘어감
  4. gatewayeureka서버로 부터 캐싱

  5. gateway캐싱되면 resource1서버를 종료시키고 배포 시작

    • resource1서버가 배포가 끝나고 시작이 될 때 eureka서버에 up으로 등록

환경 설정


Eureka Server


Eureka Server인스턴스 등록

Image

java 설정


pom.xml



<properties>  
    <java.version>11</java.version>  
    <spring-cloud.version>2021.0.8</spring-cloud.version>  
</properties>  

<dependencyManagement>  
    <dependencies>  
        <dependency>  
            <groupId>org.springframework.cloud</groupId>  
            <artifactId>spring-cloud-dependencies</artifactId>  
            <version>${spring-cloud.version}</version>  
            <type>pom</type>  
            <scope>import</scope>  
        </dependency>  
    </dependencies>  
</dependencyManagement>
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-actuator</artifactId>  
</dependency>
<dependency>  
    <groupId>org.springframework.cloud</groupId>  
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>  
</dependency>

EurekaApplication


import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;  

@EnableEurekaServer  
@SpringBootApplication  
public class EurekaApplication {  

    public static void main(String[] args) {  
        SpringApplication.run(EurekaApplication.class, args);  
    }  
}

SecurityConfig


@EnableWebSecurity(debug = false)  
@Configuration  
public class SecurityConfig {  
    @Bean  
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {  
        http  
                .csrf()  
                .disable()  
                .authorizeRequests()  
                .anyRequest().authenticated()  
                .and()  
                .httpBasic();  

        return http.build();  

    }}

application.properties


server.shutdown=graceful  
spring.lifecycle.timeout-per-shutdown-phase=10s
eureka.client.register-with-eureka=false  
eureka.client.fetch-registry=false

application-prod.properties


spring.security.user.name={입력 할 아이디}  
spring.security.user.password={입력 할 비밀 번호}  
server.port={서버 포트} 
eureka.server.my-url={서버 url}
eureka.client.service-url.defaultZone=http://${spring.security.user.name}:${spring.security.user.password}@{eureka 서버 IP 주소}:${server.port}/${eureka.server.my-url}/

Gateway


java설정


pom.xml



<properties>  
    <java.version>11</java.version>  
    <spring-cloud.version>2021.0.8</spring-cloud.version>  
</properties>  

<dependencyManagement>  
    <dependencies>  
        <dependency>  
            <groupId>org.springframework.cloud</groupId>  
            <artifactId>spring-cloud-dependencies</artifactId>  
            <version>${spring-cloud.version}</version>  
            <type>pom</type>  
            <scope>import</scope>  
        </dependency>  
    </dependencies>  
</dependencyManagement>
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-actuator</artifactId>  
</dependency>
<dependency>  
    <groupId>org.springframework.cloud</groupId>  
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>  
</dependency>

GatewayApplication

@SpringBootApplication  
@EnableDiscoveryClient  
public class GatewayApplication {  

    public static void main(String[] args) {  
       SpringApplication.run(GatewayApplication.class, args);  
    }  
}

GatewayConfig

@Configuration  
@RequiredArgsConstructor  
public class GatewayConfig {  
    private final UrlProperties urlProperties;  

    /**  
     * methodName : customRouteLocator     * author : damho-lee     * description : gateway 설정. 각각 해당하는 서버로 요청 보내준다.  
     * auth로 들어온 요청은 auth서버로 처리한다.  
     * resource 서버는 Eureka를 이용해 라운드 로빈 방식으로 동작한다.  
     *     * @param builder .  
     * @return route locator  
     */    @Bean  
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {  
        return builder.routes()  
                .route("auth_route", r -> r.path("/auth/**")  
                        .uri(urlProperties.getAuth()))  
                .route("resource-service", p -> p.path("/api/**").and()  
                        .uri("lb://RESOURCE-SERVICE")  
                )                .build();  
    }}

application.properties

spring.application.name=spring-cloud-gateway-service
eureka.client.register-with-eureka=true  
eureka.client.fetch-registry=true  

eureka.instance.prefer-ip-address=true  
eureka.instance.instance-id=gateway

Resource 서버

instance server startup.sh

result=$(curl -o /dev/null -w "%{http_code}" -X POST http://{resource ip 주소 }:{실제 서버 포트}/api/actuator/status)

유레카 서버에 resource 서버의 health 상태를 down으로 요청

echo "Result1: $result"

sleep 60 # gateway가 캐싱 되어 해당 실행된 resource 서버에 요청을 보내지 않게 기다리는 시간

docker stop resource

sleep 15 # server를 종료 시킨 후 graceful로 지정한 시간 기다림

docker rm resource

docker rmi $(docker images -q) # 서버

docker pull {docker 이미지 경로}

docker run -d --name {docker 이미지 경로} -p {java에서 지정한 port}:{실제 서버 port} -e spring.profiles.active=prod1 -e EXTERNAL_SERVER_IP={resource ip 주소} {docker 이미지 경로}

sleep 60 # 배포 후 gateway가 캐싱 되는 시간


### `java`설정

#### pom.xml
----
```java

<properties>  
    <java.version>11</java.version>  
    <spring-cloud.version>2021.0.8</spring-cloud.version>  
</properties>  

<dependencyManagement>  
    <dependencies>  
        <dependency>  
            <groupId>org.springframework.cloud</groupId>  
            <artifactId>spring-cloud-dependencies</artifactId>  
            <version>${spring-cloud.version}</version>  
            <type>pom</type>  
            <scope>import</scope>  
        </dependency>  
    </dependencies>  
</dependencyManagement>
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-actuator</artifactId>  
</dependency>
<dependency>  
    <groupId>org.springframework.cloud</groupId>  
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>  
</dependency>

ResourceApplication


@SpringBootApplication  
@EnableDiscoveryClient  
public class ResourceApplication {  

    public static void main(String[] args) {  
        SpringApplication.run(ResourceApplication.class, args);  
    }  
}

maven.yml


# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time  
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven  

# This workflow uses actions that are not certified by GitHub.  
# They are provided by a third-party and are governed by  
# separate terms of service, privacy policy, and support  
# documentation.  

name: Docker Image CI  

on:  
  push:  
    branches: [ "main", "dev" ]  
  pull_request:  
    branches: [ "main", "dev" ]  

jobs:  
  build:  
    runs-on: ubuntu-latest  

    steps:  
      - uses: actions/checkout@v3  

      # jdk 11 세팅  
      - name: Set up JDK 11  
        uses: actions/setup-java@v3  
        with:  
          java-version: '11'  
          distribution: 'temurin'  
          cache: maven  

      # 패키징  
      - name: Build with Maven  
        run: mvn -B package --file pom.xml  

      # 도커 로그인  
      - name: Log in to Docker Hub  
        uses: docker/login-action@v2  
        with:  
          username: ${{ secrets.DOCKER_USERNAME }}  
          password: ${{ secrets.DOCKER_PASSWORD }}  

      # 도커 이미지 빌드  
      - name: Build the Docker image  
        run: docker build -t newjaehun/resource .  

      # 이미지 띄우기  
      - name: push Docker image  
        run: docker push newjaehun/resource  

      # 쉘 스크립트 실행  
      - name: execute shell script  
        uses: appleboy/ssh-action@master  
        with:  
          host: ${{ secrets.SSH_IP }}  
          username: ${{ secrets.SSH_ID }}  
          key: ${{ secrets.SSH_KEY }}  
          port: ${{ secrets.SSH_PORT }}  
          script_stop: true  
          script: "./startup.sh"  

      - name: execute shell script  
        uses: appleboy/ssh-action@master  
        with:  
          host: ${{ secrets.SSH_SERVER2_IP }}  
          username: ${{ secrets.SSH_ID }}  
          key: ${{ secrets.SSH_KEY }}  
          port: ${{ secrets.SSH_PORT }}  
          script_stop: true  
          script: "./startup.sh"  

      # SonarQube 실행  
      - name: Run SonarQube  
        run: mvn sonar:sonar -Dsonar.projectKey=my-books-resource -Dsonar.host.url=${{secrets.SONAR_HOST}} -Dsonar.login=${{secrets.SONAR_TOKEN}}

ApplicationStatusController

@RestController  
@RequestMapping("/api/actuator/status")  
public class ApplicationStatusController {  

    private final ApplicationInfoManager applicationInfoManager;  
    private final ApplicationStatus applicationStatus;  

    public ApplicationStatusController(ApplicationInfoManager applicationInfoManager, ApplicationStatus applicationStatus) {  
        this.applicationInfoManager = applicationInfoManager;  
        this.applicationStatus = applicationStatus;  
    }  
    @PostMapping  
    @ResponseStatus(value = HttpStatus.OK)  
    public void stopStatus() {  
        applicationInfoManager.setInstanceStatus(InstanceInfo.InstanceStatus.DOWN);  
        applicationStatus.stopService();  
    }  
}

ApplicationStatus

@Component  
public final class ApplicationStatus {  
    private boolean status = true;  

    public void stopService() {  
        this.status = false;  
    }  
    public boolean getStatus() {  
        return status;  
    }}

CustomHealthIndicator

@Component  
public class CustomHealthIndicator implements HealthIndicator {  
    private final ApplicationStatus applicationStatus;  

    public CustomHealthIndicator(ApplicationStatus applicationStatus) {  
        this.applicationStatus = applicationStatus;  
    }  
    @Override  
    public Health health() {  
        if (!applicationStatus.getStatus()) {  
            return Health.down().build();  
        }        return Health.up().withDetail("service", "start").build();  
    }}

Properties


server.shutdown=graceful  
spring.lifecycle.timeout-per-shutdown-phase=30s  
spring.application.name=resource-service
eureka.instance.lease-renewal-interval-in-seconds=30  
eureka.client.fetch-registry=true  
eureka.client.register-with-eureka=false
management.health.status.order=DOWN, UP
management.health.status.order=DOWN, MAINTENANCE, UP
management.endpoint.jolokia.enabled=true  
management.endpoint.metrics.enabled=true  
management.endpoint.pause.enabled=true  
management.endpoint.resume.enabled=true  
management.endpoint.restart.enabled=true  
management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=*

만난 문제들

newjaehun commented 7 months ago

-L4 관련 설명 https://github.com/nhnacademy-be4-My-Books/etc/issues/129