728x90

메시지 큐란?

메시지 큐 사이 비동기 통신 프로토콜을 제공 송신자  수신자 가 같은 시간에 메시지 큐와 상호 작용 필요가 없습니다 메시지를. 큐에 배치 된 메시지는받는 사람이 검색 할 때까지 저장됩니다.

메시지 큐 패러다임은 pub-sub 패턴 의 형제입니다 . 그러나 pub-sub 패턴을 사용하면 게시자 라고하는 메시지 발신자 가 어떤 구독자가 존재하는지 알지 못해도 채널을 통해 구독자 라고하는 수신자 에게 메시지를 게시 할 수 있습니다. 모든 구독자는 수신 된 메시지가 동시에 메시지를 수신 할 수있는 시점에 존재합니다..

 

Redis는 pub-sub 패턴 을 명시 적으로 지원합니다 . 그러나이 기사에서는 Redis의 List 기본 제공 유형을 사용하여 Message Queueing을 구현하는 방법을 살펴 봅니다.

 

게시물 끝에서 Redis가 지원하는 Job Dispatcher (Nodejs) 및 Job Consumer (Golang)를 구현하는 방법을 살펴 보겠습니다.

Redis의 List 데이터 구조에 대해 알아보기

Redis에는 삽입 순서로 정렬 된 문자열 목록 인 내장 목록 데이터 유형이 있습니다. 목록 LPUSH의 맨 앞 ( RPUSH) 또는 꼬리 ( )에 요소를 밀어 넣을 수 있습니다 .

LPUSH mylist a   # now the list is "a"
LPUSH mylist b   # now the list is "b","a"
RPUSH mylist c   # now the list is "b","a","c" (RPUSH was used)

다음은 목록에 사용할 수있는 모든 요소입니다.

목록 명령 (확대하려면 클릭)

redis-cli 작업

이 게시물에서는 기본 메시지 대기열 기능 을 구현 하기 위해 PUSH  POP 작업을 사용하는 방법을 살펴 봅니다 . AWS의 다음 다이어그램에 표시된 것처럼 메시지 대기열은 Redis 용 Amazon ElastiCache 의 사용 사례 중 하나 입니다.

그러나이 게시물에서는 시리즈의 첫 번째 기사에서 수행 한 로컬 Redis 설치를 사용합니다.

를 사용하여 명령을 시도해 보겠습니다. redis-cli먼저 1 부에서 Redis VM 에 연결해야합니다 .

$ vermin ssh vm_01

위와 같이 오른쪽에있는 두 개의 스택 터미널에서 실행했습니다. BLPOP jobQueue 0즉, 이름이 지정된 목록의 선두에서 POP를 실행 jobQueue하고 요소가 추가 될 때까지 영원히 기다립니다 (따라서 시간 제한 값 0).

그리고 세 번째 터미널 (오른쪽 중 하나)에서 목록 끝에 2 개의 요소 ( "Hello"및 "world")를 푸시했습니다.

두 요소를 푸시하면 각 요소가 클라이언트 세션에 의해 팝되고 있음을 알 수 있습니다.

다음 섹션에서는 Node.js와 Golang을 사용하여 간단한 작업 대기열을 구현하고 Redis를 메시지 대기열로 사용할 것입니다.

간단한 작업 대기열 구현

먼저 환경을 설정하겠습니다. Nodejs 앱을 사용하여 작업을 작업 대기열 ( job-dispatcher ) 로 발송 하고 Golang 앱을 사용하여 작업을 소비하고 작업 ( job-consumer )합니다. 디스패처와 소비자로부터 몇 가지 애플리케이션을 배포 할 것입니다.

이제 Redis VM 내부에 로컬 파일 시스템 디렉터리를 마운트 할 수 있는 vermin의 한 기능을 사용해 보겠습니다 . 호스트 OS 콘솔에서 다음을 작성하십시오.

$ mkdir -p ~/temp/mq
$ vermin mount vm_01 ~/temp/mq

호스트 OS에 코드를 작성한 다음 VM 내에서 실행 해 보겠습니다.

IntellijIDEA를 사용하지만 원하는 편집기를 사용할 수 있습니다 (VSCode도 좋은 선택입니다).

내부에 두 개의 디렉토리를 만들 것입니다 ~/temp/mq.

$ mkdir -p ~/temp/mq/{job-dispatcher,job-consumer}

먼저 Golang ( go-redis 사용)에서 작업 소비자를 구현 하고 redis-cli클라이언트 에서 메시지를 전송하여 테스트합니다 .

다음은 소스 코드입니다.

// ~/temp/mq/job-consumer/main.go
package main

import (
   "fmt"
   "github.com/go-redis/redis/v7"
   "log"
   "time"
)

const key = "myJobQueue"

func main() {

   c := redis.NewClient(&redis.Options{
      Addr: "localhost:6379",
   })

   fmt.Println("Waiting for jobs on jobQueue: ", key)

   go func() {
      for {
         result, err := c.BLPop(0*time.Second, key).Result()

         if err != nil {
            log.Fatal(err)
         }

         fmt.Println("Executing job: ", result[1])
      }
   }()

   // block for ever, used for testing only
   select {}
}
$ GOOS=linux go build
vermin@verminbox:~$ /vermin/job-consumer/job-consumer
Waiting for jobs on jobQueue:  myJobQueue
vermin@verminbox:~$ redis-cli
127.0.0.1:6379> RPUSH myJobQueue "100"
(integer) 1
redis-cli를 사용하여 작업 소비자 앱에 작업 보내기

다음 단계는 Nodejs 에서 작업 디스패처를 만들고 높은 빈도로 여러 작업을 보내는 클라이언트를 시뮬레이션하는 것입니다.

작업 디스패처 구현

Nodejs 내에서 Node Redis 를 Redis 클라이언트로 사용할 것입니다 . 소스 코드는 다음과 같습니다.

// ~/temp/mq/job-dispatcher/index.js
const redis = require("redis");
const client = redis.createClient({
    address: "localhost:6379"
});

client.on("error", function (error) {
    console.error(error);
});

let myArgs = process.argv.slice(2);
let start = Number(myArgs[0])
let end = start + 10000

console.log(end)

for (let i = start; i < end; i++) {
    client.rpush("myJobQueue", i);
}

client.quit();
 

하지만 먼저 job-consumer6 개의 다른 터미널에서 6 개의 인스턴스를 실행 해 보겠습니다 . ( /vermin/job-consumer/job-consumer이전 섹션에 표시된 명령 사용 )

 

그리고 job-dispatcher다음과 같이 두 개의 터미널 에서 2 를 실행 해 보겠습니다 .

$ cd /vermin/job-dispatcher
$ node index.js 0
$ cd /vermin/job-dispatcher
$ node index.js 10000

Redis의 Lists를 Message Queue 로 사용하고이를 사용 하여 NodeJs에서 job-dispatcher 를 구현하고 Golang에서 job-consumer 를 구현하여 Job Queue 를 구축하는 방법 을 살펴 보았습니다.

RabbitMQ 또는 ActiveMQ와 같은 완전한 메시지 브로커를 가져 오는 대신 간단한 메시지 큐가 필요한 경우이 방법을 간단한 대안으로 사용할 수 있습니다.

 

Github에서 디스패처 및 소비자에 대한 소스 코드를 찾을 수 있습니다. https://github.com/mhewedy-playground/redis-mq

 

게시물에서 본 구현은 기본 작업 대기열이지만 처리중인 작업이 포함 된 처리 대기열을 포함하는 고급 작업 대기열을 찾는 경우 명령 RPOPLPUSH및 BRPOPLPUSH설명서를 확인하여 신뢰할 수있는 대기열 사용 사례를 확인할 수 있습니다 .

 

참조 : Korean (ichi.pro)

반응형
728x90

1. 개요[편집]

수강 신청, 예매, 온라인 접수, 게시판 폭주 등 동시 접속 폭주로 발생하는 시스템 마비와 서비스 중단을 방지할 수 있는 솔루션을 말한다.

접속을 거부하거나 지연시키는 기존의 접근제어 솔루션과 달리 임계치를 초과하는 접속 요청은 대기 정보를 제공해 순차적으로 접속할 수 있도록 한 것이다. 기존 접속자의 접속을 최대한 보장함과 동시에 접속 예상시간과 대기 인원을 확인할 수 있도록 함으로써 웹 접속자의 이용 편의성도 개선한 솔루션이다.

2. 원리[편집]


서비스를 제공하는 메인 서버와 접속제어 솔루션이 위치한 대기 서버를 별개로 분리시킨 후, 모든 요청을 대기 서버로 리다이렉트 한다. 이후 접속제어 솔루션은 설정된 동시접속 수 및 초당 접속인원을 제어하며 대기순번 부여 및 API를 통한 메인 서버의 접근 권한을 제어한다.

대기순번 부여방식은 선착순 부여방식과 동시 부여방식이 있다. 선착순 부여방식은 예정된 시간부터 접속한 순서대로 순번을 부여하지만, 시간이 되자말자 새로고침 또는 접속을 하는 사람이 많아 자칫하면 대기 서버가 터져 버리거나 바이패스[1]가 작동해 메인 서버까지 터져버릴 수 있다.

동시 부여방식은 예정된 시간까지 접속해있던 모든 클라이언트에게 랜덤으로 대기순번을 부여하고, 이후 접속자는 제일 후순위로 배정하는 방법이다. 사람들이 순차적으로 접속하기 때문에 서버에 가해지는 부담은 적으나, 유리한 순번을 얻기 위해 중복 접속하는 경우가 많다.

3. 도입[편집]

대학교, 기업, 공공기관, 정부, 각종 시험 접수 관련 사이트 등.

주로 수강신청이나 명절 기차표 예매와 같이, 짧은 시간에 대량의 접속자가 몰리면서도 누락되거나 중복되는 등의 잘못된 정보가 발생하면 안 되는 상황에서 사용한다. 게임에서도 주말 저녁과 같이 접속자가 몰리면 품질 유지의 일환으로 대기열이 발생하기도 한다.

클라우드 시스템이 도입된 IT 서비스들에서는 의외로 접속제어를 찾아보긴 어렵고, 백엔드 시스템이 열악하고 각종 서버와 프레임워크로 연계된 정부와 공공기관 사이트에서 절찬리에 사용되고 있다.

4. 개발 업체[편집]

  • (주)에스티씨랩 - NetFUNNEL(넷퍼넬)
  • (주)웰컨 - TRACER v2.0
  • (주)데브와이 - MEGA-FENCE

 

퍼옴 : 나무위키 대량접속제어

반응형
728x90

✔개요

대기열 시스템의 탄생 배경

이번에 회사에서 새로운 상품을 오픈하기 위해 대대적인 마케팅?을 진행한다는 것을 전달받아 대기열 시스템을 설계, 개발을 진행했습니다. 


왜? 우리는 대기열 시스템이 필요했을까?

현재 대고객 서비스를 위한 개발 및 운영하고 있으며, 상품에 대한 주요정보 등을 코어영역와의 통신을 통해 처리하고 있다. (우리는 고객을 상대하는 채널이다.)
우리가 도입하려는 대기열 시스템은 고객의 동접을 대응하지 못할때 FIFO(선입선출) 방식으로 순차적으로 트래픽을 처리하기 위한 방안입니다.
레거시 시스템의 경우에는 On-Premise 환경으로 구성이 되어있어 트래픽이 몰릴 시 서버 확장 대응에 어려움이 있다. (오토 스케일링이 불가능) 신규 시스템의 경우에는 (클라우드 환경) 트래픽이 몰릴 시 서버 확장 대응에 용이 하다

결국에 레거시 시스템에서 병목이 발생하고... 모든 사이트는 마비가 된다. 
우리는 장애전파(Circuit Breaker)를 막기 위해 대기열 시스템을 만들자!

 


✔ 설계시 생각한 방안

  • 1안
    • MQ(Kafka)를 활용한 대기열 시스템.
    • 장점
      • 미들웨어를 활용하므로써 정확한 MQ 시스템 구성
      • 속도 및 안정성 최고
      • 구축 경험이 있어 러닝커브 적음
    • 단점
      • 기존에 도입되지 않아서 구성 시간이 필요.
      • 단발성으로 사용하기에 비용적 측면으로 비효율적.
  • 2안
    • Redis를 활용한 대기열 시스템
    • 장점
      • List, Sorted Set와 같은 자료형을 사용하면 MQ 구현가능 할 듯
      • Cache의 장점 부각, 속도 최고.
      • 러닝커브 적음
    • 단점
      • Redis가 죽으면 끝이다.

 ✅ 우리는 2안을 선택했고 시간적인 여유가 없었고, 현명한 선택이라 생각한다.

 


 시스템구성

우리는 k8s 기반의 MSA 시스템 환경을 맞추기 위해, Spring Boot 기반의 Spring WebFlux를 도입하여 사용하였다.
Spring WebFlux는 Srping MVC와는 달리 Servlet과는 관계없이 만들어졌으며, WebFlux에서의 웹서버는 기본 설정은 Netty기반이지만 우리는 Spring Boot 에서 공식 지원 내장하는 (Lightweight, WebFlux에 적합, WebSocket 지원) Undertow를 적용했다.
WebFlux를 기반으로 논블록킹(Non-blocking) 비동기(Async) 프로그래밍을 도입했으며 Redis를 활용하였는데 reactive에 맞추어 제공되는 redis-reative 라이브러리를 사용햇다.

Fornt end와의 통신은 WebSocket과 REST API를 통해 통신을 진행하였으며,
Stomp(Simple Text Oriented Mssage Protocol)을 통해 WebSocket을 효율적으로 다루기 위한 프로토콜을 적용했다.
기존 WebSocket과는 다르게 Stomp는 Publisher/Subscriber 구조로 되어있다.
(즉, 구독한 사람들에게 메시지를 요청하면 메시지를 전달하는 형태)
대기열에서 작업열로 이동하는데 사용한 Spring-Batch도 도입하였다.

  • Java 11
  • Undertow
  • AWS WebServer
  • websocket (Stomp, SockJS)
  • Spring webflux
  • Redis-reactive
  • Junit5
  • Spring Batch

 


✔ 대기열의 구성은?

대기열의 구성은 오직 Redis와 Spring Boot 기반으로 구축하였다.
Redis 자료형 중 하나인 SortedSet을 사용했으며,
SortedSet은

  • Set과 Hash가 혼합된 타입이며,
  • 하나의 Key에 Score와 Value로 구성되어 있다.
  • Score기준으로 정렬이 가능하고
  • Queue 처럼 사용 가능하다

SortedSet 채택이유는 아래와 같다.

  • 각 명령어들의 효율적인 시간복잡도 O(1) ~ O(log(N)
    • 현재순위 조회 - Element의 현재위치 확인가능 ZRANK 명령어
      • 시간복잡도 - O(log(N))
    • 대기자 전체 크기 - Value의 전체 크기 ZCARD
      • 시간복잡도 - O(1)
    • 대기열에서 이동하는 Element Range 조회
      • 시간복잡도 - O(log(N))
    • 대기열의 Element 삭제
      • 시간복잡도 - O(log(N))
      • 자료형 List을 사용했을때는 Element 삭제시 시간복잡도 O(N)

 ✅ 우리가 대기열을 구성하기에 필요한 Queue를 적용할때 충분한 요건을 가진 자료형이다.

 


  • 대기열 로직
    1. 고객 대기열 페이지 진입.
    2. 대기열 Queue에 고객 Key값 Insert
    3. Batch에서 일정 시간마다 작업업열로 이동 가능한 Capability 확인
    4. 가능한 Capability 있으면 대기열에서 작업열로 이동.
  • 대기열 페이지에 진입한 고객
    1. 현재 위치 확인, 대기열 전체 사이즈 및 작업열 이동여부 조회.
    2. 작업열 이동 가능 여부 확인.
    3. 작업열에 유효한 Key값인지 확인
    4. 기존 발급된 이력이나, 만료된 Key값인지 확인.
    5. 동접 Ticket 발부.
    6. 세션 유지.
    7. 고객 이탈시 expire을 통해 세션 삭제.

 🔥 즉, 고객은 대기열 진입시 발급된 Key 값을 통해 작업열까지 진입하고, 티켓을 발급 받으면 티켓과 key값을 통해 고객 세션을 유지한다. 고객 세션이 만료되면 작업 가능한 Capability가 생긴다.


✔ WebSocket (Stomp)

WebSocket (Stomp) 사용 이유

  • 우리는 REST API를 통해서 통신을 진행할 수 있었지만 WebSocket을 사용했다.
  • 왜?
    • HTTP 통신의 Connection에 Cost를 줄이고자 하였고, WebSocket을 적용하였을때 Handshake를 최초에만 진행하여 전체적인 네트워크 Cost를 줄일 수 있다는걸 결론으로 도출했다.
    • WebSocket은 HTTP Protocol로 Connection을 초기에 맺고, ws(WebSocket),wss Protocol로 Upgrade한 후 서로에게 Heartbeat를 주기적으로 발생시켜 Connection을 유지하고 있는지 확인한다.
    • WebSocket(Stomp)은 브라우저에 대한 호환성 때문에도 채택을 진행했다.

 💡 즉, Cost를 절감해 고객의 유입폭이 더 증가 할 수 있다 라는 결과를 뽑았다. ( HTTP API 통신을 안한 것은 아니다. 필요에 따라 사용했다. )

WebSocket (Stomp) 연결

@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private final StompHandler stompHandler;

    public WebSocketConfig(StompHandler stompHandler) {
        this.stompHandler = stompHandler;
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/topic");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/connect")
                .setAllowedOriginPatterns("*")
                .withSockJS()
                .setClientLibraryUrl("<https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.3.0/sockjs.min.js>");
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
//        registration.interceptors(new HeaderCheckInterceptor());
        registration.interceptors(stompHandler);
    }

}
  • registerStompEndpoints
    • Websocket Connection에 관련된 설정이다.
    • SockJS를 이용해 STOMP end-point를 설정해준다.
    • withSockJS()
      • 브라우저에서 Websocket을 지원하지 않을 경우 Fallback 옵션을 활성화 하는데 사용한다.
      Enable SockJS fallback options.
    • setAllowedOriginPatterns("*")
      • WebSocket에서 Cors 처리를 위한 허용 패턴.
      • 일정 버전부터 setAllowedOrigins 메서드가 사용되지 않아 setAllowedOriginPatterns을 사용함.
      Alternative to setAllowedOrigins(String...) that supports more flexible patterns for specifying the origins for which cross-origin requests are allowed from a browser. Please, refer to CorsConfiguration.setAllowedOriginPatterns(List) for format details and other considerations. By default this is not set. Since: 5.3.2
  • configureMessageBroker
    • 메시지브로커에 대한 Prefix 설정.
    • config.setApplicationDestinationPrefixes
      • Socket 통신시 End-Point 목적지의 Prefix 설정이다.
      • 즉, Client Side에서 Server 사이드로 보내는 Message
      Configure one or more prefixes to filter destinations targeting application annotated methods. For example destinations prefixed with "/app" may be processed by annotated methods while other destinations may target the message broker (e.g. "/topic", "/queue"). When messages are processed, the matching prefix is removed from the destination in order to form the lookup path. This means annotations should not contain the destination prefix. Prefixes that do not have a trailing slash will have one automatically appended.
    • config.enableSimpleBroker
      • Subscriber에게 메시지를 보낼때의 목적지의 Prefix 설정이다.
      • 즉, Server Side에서 Client Side로 보내는 Message
      Configure the prefix used to identify user destinations. User destinations provide the ability for a user to subscribe to queue names unique to their session as well as for others to send messages to those unique, user-specific queues. For example when a user attempts to subscribe to "/user/queue/position-updates", the destination may be translated to "/queue/position-updatesi9oqdfzo" yielding a unique queue name that does not collide with any other user attempting to do the same. Subsequently when messages are sent to "/user/{username}/queue/position-updates", the destination is translated to "/queue/position-updatesi9oqdfzo". The default prefix used to identify such destinations is "/user/".
  • configureClientInboundChannel
    • Inbound 메시지에 대한 Intercepter처리를 할 수 있다.
    • JWT 인증과 같은 인증 로직에 주로 이용하고 있으며, @ChannelInterceptor 어노테이션을 이용하니 필요하면 참고해보길 바란다.

publish / subscribe

@Slf4j
@RestController
public class SocketHandler {

    @MessageMapping("/sendMessage/{key}")
    @SendTo("/topic/public/{key}")
    public String hello(String str){
        log.info("Check in hello -> " + str);
        return "your message -> " + str;
    }

}
  • @MessageMapping
    • Publish Target Url
    • Client to Server
    Destination-based mapping expressed by this annotation. For STOMP over WebSocket messages this is AntPathMatcher-style patterns matched against the STOMP destination of the message.
  • @SendTo
    • Subscribers 한테 Message를 전송한다
    • Server to Client
  • {key}
    • 우리는 고객마다 고유한 End-Point를 Key값을 통해 지정했다.

Junit 을 이용한 Client Test

@ActiveProfiles("local")
class SocketControllerTest {

    final String TARGET_URI = "<http://localhost:30001/connect>";
		final String SENDMESSAGE_URI = "/app/sendMessage/123456";
		WebSocketStompClient stompClient;

    private List<Transport> createTransportClient(){
        List<Transport> transports = new ArrayList<>();
        transports.add(new WebSocketTransport(new StandardWebSocketClient()));
        return transports;
    }

    @BeforeEach
    public void setup() throws InterruptedException{
        stompClient = new WebSocketStompClient(new SockJsClient(createTransportClient()));

        stompClient.setMessageConverter(new MappingJackson2MessageConverter());
    }

		@Test
    public void contextLoad() throws ExecutionException, InterruptedException, TimeoutException {

            WebSocketHttpHeaders httpHeaders = new WebSocketHttpHeaders();
            httpHeaders.add("jwt" , "test");
            StompHeaders stompHeaders = new StompHeaders();
            StompSession stompSession = stompClient.connect(TARGET_URI, httpHeaders, stompHeaders, new StompSessionHandlerAdapter() {
            }).get(1, TimeUnit.SECONDS);

            // Send
            stompSession.send(SENDMESSAGE_URI, "test");

		}
}
  • Junit의 WebSocketStompClient을 사용하여 Server side와의 WebSocket 연동 Test를 진행.
    • 소스참고.

 


✔ Redis

대기열에 큰 Point를 가지고 있는 Redis에 대한 셋팅이다.

Redis reative 설정

@Slf4j
@Configuration
@EnableCaching
public class RedisConfiguration {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;

    
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    @Bean
    public ReactiveRedisTemplate<String, String> reactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory) {
        RedisSerializer<String> serializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(String.class);
        RedisSerializationContext serializationContext = RedisSerializationContext
                .<String, String>newSerializationContext()
                .key(serializer)
                .value(jackson2JsonRedisSerializer)
                .hashKey(serializer)
                .hashValue(jackson2JsonRedisSerializer)
                .build();

        return new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
    }

}
  • Redis를 Reative 방식으로 Template 설정.

주로 사용한 RedisUtils

@Slf4j
@Component
@AllArgsConstructor
public class RedisUtils {

    private final RedisTemplate<String ,Object> redisTemplate;

    private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;

		/**
     * @desc: Sorted Set 삭제.
     */
		public Mono<Long> delete(String key){
       return reactiveRedisTemplate.delete(key);
    }

		/**
     * @desc: Sorted Set 조회
     */
		public Mono<String> getValue(String key){
        return reactiveRedisTemplate.opsForValue().get(key);
    }

		/**
     * @desc: RedisTemplate에 SortedSet 초기화.
     */
    public ReactiveZSetOperations opsForZSet(){
        return reactiveRedisTemplate.opsForZSet();
    }

		/**
     * @desc: Sorted Set 자료형 사이즈
     */
    public Mono<Long> zCard(String str){
        ReactiveZSetOperations z = reactiveRedisTemplate.opsForZSet();
        return z.size(str);
    }

		/**
     * @desc: Sorted Set 자료형 start ~ end 까지 조회.
     */
    public Flux<String> zRange(String key, Long start, Long end){
        return opsForZSet().range(key, Range.closed(start, end));
    }

		/**
     * @desc: Sorted Set 자료형 Value의 현재위치 조회.
     */
    public Mono<Long> getzRank(String key, String value){
        return opsForZSet().rank(key, value);
    }
}

 


✔ 스트레스 테스트

고민한 설계와 개발을 끝냈으니 성능 테스트를 진행했다.
테스트 Tool 선택에 대한 고민이 컸다. nGrinder, JMeter들을 사용했으나 정확한 테스트가 진행되지 못했고, JUnit을 통해 부하 테스트를 진행했다.

시나리오

  • Connection → Subscribe → Send 와 같은 시나리오로 부하 테스트를 진행했다.

Stress Size

  • 약 15분간 총 12만건 진입
    • 분당 약 7천명
    • 초당 약 120명

결과

아래 수치를 보면 더 정확하게 확인이 가능하다.

  • Redis는 역시나 강력했고, 12만명의 동접자를 충분히 버틸 수 있는 수치가 측정되었다.
  • CPU와 RAM이 20% 까지는 쳤지만 이정도 수치를 3~4배로 가정해도 서비스가 버틸 수 있는 수치이다.

 


 

✔ 마무리

상품을 오픈하고 대기열 서비스를 오픈했었고, 큰 이슈 없이 프로모션들을 넘어갔다. 그리고 아쉽게 죽어있는 상태 ㅎㅎ
많은 사람들이 레거시에 대한 문제와 기술 도입에 대한 보수적인 의견을 가지고 있다고 생각한다.
틀린말은 아니다. 그걸 해결하고 극복하기 위해 내가 존재하며, 좋은 결과물을 도출해낼 것이다.

 

퍼옴 :

제제의 개발 발자취

반응형

'BigData > 웹대기' 카테고리의 다른 글

Redis를 사용하여 작업 대기열 구현  (0) 2023.04.03
대량접속제어 기본개념 (나무위키)  (0) 2023.04.03
728x90

import pandas_datareader.data as web

import datetime

import matplotlib.pyplot as plt

from zipline.api import order, symbol

# order : zipline 백테스팅 시뮬레이션 주문 실행 함수

# symbol : 참조할 데이터에 대한 심볼 등록

#from zipline.algorithm import TradingAlgorithm

from zipline import run_algorithm

from zipline.utils.factory import create_simulation_parameters

# create_simulation_parameters : 초기 금액 설정에 사용

def initialize(context):

pass

def handle_data(context, data):

# order을 통해 AAPL 심볼 주식을 1주 매수

order(symbol('AAPL'), 1)

start = datetime.datetime(2010, 1, 2)

end = datetime.datetime(2016, 3, 19)

data = web.DataReader("AAPL", "yahoo", start, end)

# 새로운 dataframe 객체 만들기

data2 = data[['Adj Close']]

# dataframe의 column의 이름 바꾸기

data2.columns = ['AAPL']

data2 = data.tz_localize("UTC")

data2.head()

# sim_params = create_simulation_parameters(capital_base=100000000)

algo = run_algorithm(start = data2.index[0], end = data2.index[-1], capital_base = 1000000, initialize=initialize, handle_data=handle_data)

plt.plot(algo.index, algo.portfolio_value)

plt.show()

-----

파이썬으로 배우는 알고리즘 트레이딩에 있는 예제에서 변경,

Tradingalgorithm 함수는 과거의 zipline api의 함수인 것으로 확인,

run_algorithm으로 변경

algo = run_algorithm(start = data2.index[0], end = data2.index[-1], capital_base = 1000000, initialize=initialize, handle_data=handle_data)

맨 끝에 data = data 를 추가했을 경우에는 에러 발생.

이것에 대한 이유는 차차 확인해보기로.

 

[출처] Zipline 1.4.1 버전 오류 해결|작성자 PKB

반응형
728x90

Log4j에서 LOGBack으로 마이그레이션 하기 ( migrate from log4j to logback)

몇년전 부터 프로젝트에서 로그 라이브러리로 “LOGBack“를 사용하고 있습니다.

혹시나 모르시는 분을 위해서 간략하게 설명하면 “LOGBack“은
Log4J“, “Apache Common Logging“, “Java Logging” 같은 “Logger” 라이브러리 입니다.
또한 “Log4J“를 만든 개발자가 만들어서 “Post Log4J“라고도 합니다.

보다 자세한 내용은 예전에 제가 포스팅 했던

logback을 사용해야 하는 이유 (Reasons to prefer logback over log4j)

을 참고 하시기 바랍니다.

프로젝트를 신규로 만들때는 “LOGBack” 라이브러리 와
logback.xml (log4j.xml와 비슷한 설정 파일)” 파일 두 가지만 있으면
쉽게 적용이 가능 합니다.

하지만 대부분 기존 시스템들은 “Log4J“를 이미 사용하고 있거나 또는 본인 의지와 상관 없이
3rd 라이브러리들이 내부적으로 사용하고 있는 경우 일 것입니다.
이럴 경우 몇가지 주의해야 할 사항에 대해서 말씀 드리겠습니다.

# SLF4J

마이그레이션에 대한 설명을 드리기 전에 “SLF4J“에 대한 이해가 필요 합니다.
SLF4J의 핵심은 다양한 Logger 구현체들을 추상화를 통해서 언제든지 교체 가능 할수 있도록 하는 것입니다.
즉,  “Logger Facade” 라이브러리 입니다.

concrete-bindings

(이미지 출처 : http://www.slf4j.org/images/concrete-bindings.png)

위의 그림을 보게되면 모든 “어플리케이션“은 구현체를 직접 호출하는 것이
아니라 “SLF4J API를 호출하게 됩니다.

SLF4J는 전달받은 메세지 정보를 실제 구현체로 전달 합니다.

pseudocode 코드로 표현 하면

1
2
3
4
5
6
7
8
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
private static final Logger LOGGER = LoggerFactory
            .getLogger(Dummy.class);
public void echo () {
   LOGGER.info("hello SLF4J");
}

어플리케이션을 개발하는 사람은 “SLF4J API를 사용하며, 여기에는 실제
구현체에 대해서 명시하지 않습니다.

이렇게 API로 랩핑하기 때문에 “Logger 구현체“를 쉽게 교체할수 있도록 합니다.

Bridge Log4J

Migration“을 가장 쉽게 하는 방법은 단지 “Log4J” 라이브러리를  제거하고 “LOGBack“라이브러리를
추가 하는 것입니다.

하지만 문제는 3rd (외부 라이브러리)가 직접 구현체 “Logger”를 호출한다는 것입니다.

pseudocode 코드로 표현 하면

1
2
3
import org.apache.log4j.Logger;
...
private static Logger logger=Logger.getLogger("LoggingExample");

직접 작성한 소스인 경우는 요새 워낙 IDE가 제공하는 “refactoring” 기능이 좋아서
금방 작업을 할 수 있지만, 외부 라이브러리 같은 경우는 “어플리케이션 제어권 밖”
이기 때문에  난처한 상황이 발생 합니다.

혹시나 해서 본인의 어플리케이션을 구동해서 에러가 없다 하더라도
논리적으로 이러한 “Issue“는 잠재적으로 발생한다는 것입니다.

저 같은 경우는 Junit을 실행할때는 문제가 없다가 전체 어플리케이션을
구동할때 “ClassNotFoundException“가 발생했던 기억이 나며, 생각보다
해당 “Issue“를 디버깅하기는 쉽지 않습니다.

제가 말씀드리고 싶은건  마이글이션시 SLF4J를 사용하던, Log4J 구현체를 사용하건
어플리케이션을 다쳐서는 안된다는 것입니다.

SLF4J“에서는 다행히 이러한 이슈를 해결하기 위해서 “extend” 라이브러리를 제공 합니다.

slf4j-with-jcl-and-log4j

(이미지 출처 : http://espenberntsen.files.wordpress.com/2010/06/slf4j-with-jcl-and-log4j.png)
log4j-over-slf4j.jar” 파일을 압축해제하면 “log4j.jar” 와
동일한 패키지, 클래스가 존재 합니다.
즉, 패키지, 클래스를 동일하게 해서 구현체 클래스를 fake하는 것입니다.

그렇게 호출된 로그정보(메세지, 로그레벨…) 는 SLF4J에게 전달하고 바인딩된
LOGBack“이 실제로 로그를 Write하는 것입니다.

Maven (pom.xml)

Migration“에서 필요한 라이브러리를 아래와 같이 추가 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
...
 
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>1.0.13</version>
 </dependency>
 <dependency>
<groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.0.13</version>
 </dependency>
 <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.0.13</version>
 </dependency>
 <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.5</version>
 </dependency>
 <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.5</version>
 </dependency>
 <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.5</version>
 </dependency>
 <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jcl</artifactId>
    <version>1.7.5</version>
 </dependency>
...

logback.xml

logback.xml (설정 파일)”을 “classpath“경로에 생성 합니다.

이미지 2--

기본 설정을 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
 
<root level="debug">
<appender-ref ref="STDOUT" />
 </root>
</configuration>

multiple SLF4J bindings

모든 설치/설정이 끝나고 “어플리케이션”를 구동하면 에러 없이 정상적으로 기동이 되는데
LOGBack” 설정이 안되는 경우가 있습니다.

그런 이유는 “SLF4J” 인터페이스를 구현한 구현체가 아직까지 존재하기 때문입니다.
아래 그림을 보면 SLF4J가 구동하면서 binding 대상을 scan
합니다.
내부적으로는 깊게 봐야하겠지만 현상으로 보면 여러 구현체 중에서 첫번째 것을
채택을 합니다.

이미지 3

그래서 “Log4J” 라이브러리를 혹시 참조하는 것이 있는지 확인을 하고, 혹시 있다면
exclude를 해야 합니다.

※ Log4J 관련된 라이브러리들만 exculde 해야 합니다.

라이브러리 관계 와 Exclude를 하는 것은 쉽지가 않습니다. 하지만 Eclipse에서 제공하는
Maven 플러그인을 사용하면 좀더 쉽게 수정 할 수 있습니다.

이미지 5

오른쪽 에디터에서 중복된 라이브러리를 체크 후 exclude를 하면서
왼쪽 라이브러리 path에서 빠져있는지 동시에 확인 합니다.

Migration“이 정상적으로 되었다면 아래와 같이 “LOGBack” 메세지를 확인 할수 있습니다.
이미지 6

conclusion

지금까지 “Log4J“에서 “LOGBack“로 “migration” 하는 방법에 대해서 설명 드렸습니다.
Logger 코드”는 소스 대부분에 많이 삽입이 되어 있어서 반드시 꼼꼼하게 확인이 필요하며
“과연 마이그레이션을 해야하는 것인가?”에 대한 충분한 팀 협의가 필요하고
만약 마이그레이션을 한다면 본 포스팅이 조금이나 도움이 되었으면 합니다.


반응형

'BigData > LogBack' 카테고리의 다른 글

1. logback 사용해야 하는 이유  (0) 2016.01.20
728x90

logback 사용해야 하는 이유 (Reasons to prefer logback over log4j)


출처 : https://beyondj2ee.wordpress.com/2012/11/09/logback-사용해야-하는-이유-reasons-to-prefer-logback-over-log4j/

최근 프로젝트를 진행하면서 자바 로깅 구현체(logger)로  “LOGBack“을 사용 하고 있습니다.

개발자의 얼리“적 성향이기 보다는 “log4j“에서 제공을 하지 않는기능 외에 다양한 이점이 있기 때문 입니다.
아시는 분들도 있지만, 모르시는 분들을 위해서 간략하게 말씀 드리자면

Log4j” (현재는 Apache Logging Service라는 Top Project)는  Ceki Gülcü라는
개발자가 최초로 만들었습니다.
Log4J“는 java world에서 “가장 많이 사용하고 있는 logger“라고 감히 말씀 드릴수 있습니다.

이러한 성공에 힘을 입어 “Ceki Gülcü“는 좀더 “Logger“에 대해서 깊은 프로젝트를 시작했고,
그것이 바로 “SLF4J” 와 “LOGBack” 입니다.

SLF4J“는 “로깅 구현체“라기 보다는 “Logging Facade” 입니다.
(※ facade pattern 참조)

일명 창구 일원화” 패턴인데, “SLF4J“의 API를 사용하면, 구현체의 종류와 상관없이
일관된 로깅 코드“를 작성할 수있으며,  “Apache Commons Logging“를 사용하다가
Log4J“로 변경을 할 경우 최소한의 수정으로 구현체를 교체 할수 있습니다.

오늘 설명하는 “LOGBack“은이중 하나의 “구현체” 입니다. 그래서 과연
어떤 이점이 있는지 quick 하게 둘러보고자 합니다.

해당 내용은 (Reasons to prefer logback over log4j)의 자료를 참고 했습니다.

(1) Faster implementation

LOGBack“은 새롭게 작성 한것이 아니라, 오랫동안 검증된 “LOG4J”의 아키텍쳐 기반으로
재작성 되었습니다.
특히 성능은 약 10배 정도 빠르게 개선을 했고, 또한 메모리 점유도 보다 적게 사용을 합니다.
역시 성능에 관련된 얘기 입니다.

(2) Extensive battery of tests

위에서 말씀 드렸듯 “새로운 오픈소스” 이지만 , 이미  “Log4j” 시절 부터 수년간 수 많은 광범위한 테스트
를 진행했고, 더욱더 많은 테스트의 반복 과정을 수행 했습니다.
Log4j보다 훨씬 뛰어난 가장 큰 이유중 하나“라고 합니다.
그 만큼 “높은 쿼리티“에 대한 자신감을 엿볼수 있습니다.

(3) logback-classic speaks SLF4J natively

LOGBack“은 크게 3가지 컴포넌트로 구성이 되어 있습니다.
logback-core“는 말 그대로 핵심 코어 컴포넌트 입니다.
logback-classic“은 “slf4j“에서 사용이 가능하도록 만든 플러그인 컴포넌트 입니다.
logback-access“는 사용하는 어플리케이션이 “웹 어플리케이션“일 경우 빛을 바라는
컴포넌트 입니다.” HTTP 요청에 대한 강력한 디버깅 기능을 제공 합니다.

(4) Extensive documentation

오픈소스 선택“에 있어서 레퍼런스 메뉴얼은 상당히 중요한 포인트 입니다.
LOGBack“은 지속적이면서, 상세한 메뉴얼을 제공 합니다.

(5) Configuration files in XML or Groovy

logging 설정에 대한 syntax는 기본적으로 “XML” 형태로 되어 있습니다.
또한 Groovy의 “syntax” 를 지원 합니다.

(6) Automatic reloading of configuration files

개인적으로 호감이 가는 기능중 하나 입니다. 일하는 도메인 마다 틀리겠지만
기본적으로 “운영 서버” 모드에서는 로그레벨을 “WARN” 또는 “ERROR” 입니다.

하지만 만약 운영중에 좀더 상세한 로그를 보기 원할 경우가 있습니다.
예를 들어서 “INFO” 레벨로 변경하는 경우 입니다.

log4j” 같은 경우는 다음과 같이  “서버를 셧다운 -> 재설정 -> 서버 기동” 의
절차로 진행을 합니다. 즉, 핵심 포인트는 “서버를 재기동” 해야 한다는 것입니다.

이러한 매커니즘은 “내부 스캐닝하는 별도의 쓰레드“가 감지를 하기 때문 입니다.
하지만 이런 “스캐닝 쓰레드”는 메모리에 대한 점유율을 최소화 하며,
심지어 “100개의 쓰레드가 초당 백만 invocation“을 발생해도 시스템에
크게 무리를 주지 않는 다고 합니다.

주로 이런 기능은 “WAS에서 동장하는 웹 어플리케이션“에서 많이 사용이 될듯 합니다.

(7) Graceful recovery from I/O failures

Log4j” 같은 경우  “JDBC” , “Socket“등 다양한 “Appender“를 지원 합니다.
특히. 대다수의 환경에서는 “File”에 제일 많이 로그를 저장 할 것입니다.

하지만 만약 “파일 서버가 일시적으로 장애가 발생 할경우” , “파일 서버가 복구 될때까지
어플리케이션을 중지 시켰다가” 다시 파일 서버가 복구되면, 그때 서버를 재기동 할것 입니다.
하지만 “LOGBack“은 “서버 중지 없이, 이전 시점부터 복구를 graceful “하게 지원을 합니다.

(8) Automatic removal of old log archives

대부분 환경에서는 “하나의 파일”에 기록하는 것이 아니고, “특정 시간” 또는 “특정 파일 사이즈
로 “Rolling” 해서 “Archiving“을 할 것입니다.

하지만 “Archiving된 로그 파일“을 계속 유지하지 않고, 일정 기간 지나면
서비스에  부담을 주지 않기 위해서” 파일을 삭제 할것 입니다.

이럴경우 “Crontab” 또는 다른 삭제 스케줄러를 설정 또는 개발을 할것입니다.
LOGBack“은 “maxHistory“라는 설정 정보를 통해서 주기적으로 “Archive
파일을 자동 삭제 합니다.
maxHistory“의 값이 “12“일 경우, “12개월 이후 삭제 하라는” 뜻입니다.

(9) Automatic compression of archived log files

Archived Log File“를 만들때 비동기적으로 롤링 할 동안
자동 압축을 실행 합니다. 심지어 파일 용량이 커도, 또한
이러한 압축 작업은 어플리케이션을 block 시키지 않습니다.

(10) Prudent mode

만약 하나의 서버에서 “다수의 JVM“이 기동중일 경우 “하나의 파일“에
로그를 수집하려고 할때 사용하는 기능 입니다.

즉, “Log Aggregation” 기능 입니다. 조금만 아이디어를 내면, 매우 유용하게
사용이 가능 합니다.
다만 서로 쓰레드간 writing 할때 경합이 생기기 때문에 대량의 데이터를 사용할때
는 다소 피해야 합니다.

그렇기 때문에 반드시 해당 옵션을 적용시 “LOGBack” 메뉴얼을 참고 하시기 바랍니다.

(11) Lilith

Lilith“은 “현재 발생하는 로그 이벤트에 대한 상태 정보를 볼수 있도록 지원 하는 
뷰어” 입니다.
log4j“를 지원하는 “chainsaw” 와 비슷한 기능을 합니다. 다만 차이점은 “Lilith
Large data“를 지원 합니다.

(12) Conditional processing of configuration files

빌드를 해본 사람이라면 아실듯 합니다. 빌드시 제일 골치가 아픈 것이 “config” 정보 와
log file path” 입니다.
이유는 어플리케이션이 구동하는 환경이 “로컬, staging, cbt” 마다 틀리기 때문 입니다.
이런점을 해결 하기 위해서 “Maven Filter” 기능을 사용 하거나,  “JVM -D환경변수“를
통해서 각 환경 마다 선택적으로 “log 설정파일“을 선택하도록 구성을 할 것입니다.

LOGBack“은 이런 이슈를 좀더 “Graceful“하게 지원을 합니다.
JSTL” 같이 분기 스크립트 (<if><then> and <else>)를 제공해서 하나의 파일
에서 다양한 빌드 환경을 제공 하도록 지원을 합니다.

(13) Filters

Filter” 기능은 “Log4j“에서 지원하는 기능입니다. 주 기능은 “로깅을 남길지 , 말지를”
핸드링 할수 있는 기능 입니다.
좀더 이해를 돕기 위해서 “UseCase“를 설명 드리겠습니다.

만약 “A“라는 사용자에게 비즈니스적으로 문제점이 발견이 되었습니다.
이럴경우는 “logical exception“이기 때문에 원인을 잡기가 쉽지가 않습니다.
또한 현재 “운영 서버“의 “Log LEVEL“이 “ERROR“일 경우 로그를 확인 할수가 없을
것입니다.
더우기 “운영 서버 logical exception“은 “staging” 환경과 데이터가 틀리기 때문에
실제 운영 로그를 확인하지 않고서는 재현을 할수가 없습니다.

그래서 “운영 서버의 로그 레벨“을   “DEBUG“로 수정합니다. 하지만 로그는 보이지만 전체 사용자가
해당이 되기 때문에 로그 데이터를 분석하기가 어렵습니다.
이럴 경우 “Filter“를 사용해서 “다른 사용자“는 “ERROR” 레벨을 유지하고,
“A” 사용자만 “DEBUG“로 설정을 하면, 분석하는데 많은 도움을 받을 수 있습니다.

(14) SiftingAppender

SiftingAppender“는 “Filter“의 기능과 유사하면서 다른 기능을 제공 합니다.
로그 파일을 특정 주제별로 분류“를 할 수 있도록 합니다.
예를 들어서 “HTTP Session“별로 로그 파일을 저장한다거나, “사용자별“로 별도의 로그파일을
저장 할 수 있습니다.

(15) Stack traces with packaging data

자바 어플리케이션에서 “Exception“이 발생을 하면 “Stack Trace“를 출력을 합니다.
자바는 “Exception” 정의가 잘되어 있어서 쉽게 “디버깅“을 할 수 있습니다.

하지만 제일 디버깅이 힘든 것중 하나가 “라이브러리에 의한 Exception” 입니다.
이럴 경우 “LOGBack“은 Exception 발생시 참조 했던 외부 라이브러리 버전을
출력하게 해줍니다.

(16) Logback-access, i.e. HTTP-access logging with brains, is an integral part of logback

위에서 언급했듯이 “logback-access“는 “웹 어플리케이션” 사용시 유용한 툴로써 제공 됩니다.
특히나 요새는 전통적인 “HTML” 출력이 아닌 “REST 서버“로써의 역할을 많이 합니다.
이럴 경우 “HTTP 디버깅“을 제공 합니다.

Conclusion

지금 까지 간략하게 “LOGBack” 기능을 살펴 봤습니다.
이외에 유용하고, 멋진 기능들이 많이 있습니다.

logging“의 중요함은 지나치게 말씀을 드려도 지나치지 않습니다.
특히나 “클라우드 빅데이터” 시대에서 “logger“는 바로 최초 시작점 입니다.

제일 궁금했던 것이 “log4j를 만든 사람이 왜 ? logback를 만들었을까?” 입니다.

개인적 생각은 “log4j“를 “하위 호환성을 유지하면서 큰 리팩토링을 하기에는 다소 부담감
과 “상용 버전을 위한 비즈니스적 관점“이 아닐까 조심 스럽게 말씀 드리고 싶습니다.

LOGBack“의 큰 매력은 “Log4J 와 상당히 친숙함” 일것입니다. 이말은 그 만큼 사용하는데
있어서 낯설지 않다는 얘기 입니다.
심지어 “log4j.properties“를 “logback.xml” 설정 파일로 변환하는 “웹 변환기“도 제공 합니다.

또한 “SLF4J“를 지원하기 때문에 마음에 들지 않으면 “언제든지 다른 로거로 swiching“이 가능 합니다.


반응형

+ Recent posts