문제

연계기관에서 API를 통해 데이터를 가져가는 케이스가 있다. 

데이터를 가져가게 되면 TRAN_YN을 'Y'로 update 하는데 테스트 요청 시 마다 'N'으로 업데이트를 해주어야한다...

... 나는 언제 퇴근 하라고...

 

해결

MYSQL에 Event기능이 있다고 하여 추가 하였다.

 

이벤트 생성

CREATE EVENT IF NOT EXISTS FRANS_SET
 ON SCHEDULE
   EVERY 1 MINUTE
   START '2024-06-20 15:30:00'
  DO UPDATE TBL_XXXX SET TRANS_YN ='N'

 

이벤트 조회

SHOW EVENTS;

 

이벤트 삭제

DROP EVENT TRAN_SET

 

 

얏호 퇴근하자.

Kafka로 부터 동일한 데이터가 3번 들어오는 케이스가 있었음.

 

어플리케이션의 동작이 정상적이지 않아 로그를 확인해보니 Kafka로부터 동일한 데이터가 3번 들어오는 케이스가 있었다.

해당 데이터를 받아 DB에 insert를 하는 작업인데 동일한 pk로 3개의 데이터가 들어오니 Duplicate PK 관련 에러가 발생하였다.

 

모니터링 툴을 사용하여 확인한 결과 DB 팀에서 DB 변경 Alter Table을 수행한 것이 원인으로 테이블에 대한 lock으로 인해 처리를 하지 못하고 데이터가 리밸런싱 되었다.

개요 

여러 서비스들의 배치 수행시간을 컨트롤하고 배치 성공, 실패 로그를 수집해야하는 작업이 주어졌다.

해당 업무를 진행 하기 전 테스트를 위하여 진행하고 있던 웹 포트폴리오에서 테스트를 진행하고자 한다.


구성 및 설계

기존은 @shceduled를 사용하여 특정 시간에 배치를 수행하였지만 배치 수행시간을 변경하려면 소스의 수정 및 배포가 강제되었다. 이를 해결하기위해 해당 배치들을 API 형식으로 수정하고 Jenkins의 스캐줄을 사용하여 배치수행을 진행한다.

 

또한 단일 서비스가 아닌 여러 서비스의 배치 통합이 이루어져야 하기 때문에 각각의 Batch 수행에 관련된 로그를 수집하여야 한다. 이를 해결하기위해 Elasticsearch와 Logstash를 사용한다.

 

 


Elasticsearch, Logstash 설치

Docker를 사용하여 설치

# ELK가 세팅된 git 프로젝트 clone
git clone https://github.com/ksundong/docker-elk-kor.git

# logstash 데이터를 확인하기위한 volum 디렉토리 생성
mkdir logs

 

 

logstash데이터 확인을 위한 logs 볼륨 추가

docker-compose.yml

      context: logstash/
      args:
        ELK_VERSION: $ELK_VERSION
    volumes:
      - type: bind
        source: ./logstash/config/logstash.yml
        target: /usr/share/logstash/config/logstash.yml
        read_only: true
      - type: bind
        source: ./logstash/pipeline
        target: /usr/share/logstash/pipeline
        read_only: true
        # logstash input을 확인하기위한 볼륨
      - type: bind
        source: /home/k/ELK/docker-elk-kor/logs
        target: /logs
    ports:
      - "5044:5044"
      - "5000:5000/tcp"
      - "5000:5000/udp"
      - "9600:9600"
    environment:
      LS_JAVA_OPTS: "-Xmx256m -Xms256m"
    networks:
      - elk
    depends_on:
      - elasticsearch

  kibana:
    build:
      context: kibana/
      args:
        ELK_VERSION: $ELK_VERSION
    volumes:
      - type: bind
        source: ./kibana/config/kibana.yml
        target: /usr/share/kibana/config/kibana.yml
        read_only: true
    ports:
      - "5601:5601"
    networks:
      - elk
    depends_on:
      - elasticsearch

networks:
  elk:
    driver: bridge

volumes:
  elasticsearch:

 

 

 

 

logstash 파일 출력 및 기타설정

logstash/pipeline/logstash.conf

input {
# beats 미사용
       # beats {
        #        port => 5044
       # }

        tcp {
                port => 5000
        }
}

## Add your filters / logstash plugins configuration here

output {
        elasticsearch {
                hosts => "elasticsearch:9200"
                index => "logs"
                user => "username"
                password => "password"
                ecs_compatibility => disabled
        }
		# 파일 출력을 위한 설정
        file {
                path => "/logs/test.log"
        }

}

 

 

Logstash, Elasticsearch 기동

docker-compose build && docker-compose up -d

 

 

 

키바나 확인 

http://localhost:5601/app/home#/ 

 

 


Spring boot의 Logback 세팅

module-common의 build.gradle

dependencies {
    // 이미 board에 포함된 의존성임으로 추가하지 않음
    implementation project(":module-common")
    implementation project(":module-normal-board")

	// logback을 사용하여 logstash로 전달하기 위하여 필요한 의존성
    implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
    implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.5.6'
}

 

Logback 세팅

module-main의 ressources에 logback.xml 생성

 

logback.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>

<!-- 콘솔에 로그를 출력하는 콘솔 앱렌더 설정 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <!-- 로그 출력을 위한 레이아웃 설정 -->
    <layout class="ch.qos.logback.classic.PatternLayout">
        <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
    </layout>
</appender>

<!-- 파일에 로그를 저장하는 파일 앱렌더 설정 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 일별로 로그 파일 롤링 -->
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 보관할 로그 파일의 최대 개수 -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
        </layout>
    </appender>

	<!-- 로그스태시 세팅, 로컬의 5000포트 사용-->
    <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>127.0.0.1:5000</destination>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
        <layout class="net.logstash.logback.layout.LogstashLayout">
            <timestampPattern>yyyy-MM-dd' 'HH:mm:ss.SSS</timestampPattern>
        </layout>
    </appender>


<!-- 루트 로거 설정 -->
<root level="INFO">
    <appender-ref ref="CONSOLE" />
<!--    <appender-ref ref="FILE" />-->
</root>

<!-- 로그스태시 사용 -->
<logger name="LOGSTASH">
    <appender-ref ref="LOGSTASH"/>
</logger>

</configuration>

 

 

module-main의 @EnableAsync 설정

API를 호출 했을 때 배치가 오래 걸리린다면(예를들어 1시간 돌아가는배치) request에 대한 timeOut이 발생하기 때문에 비동기로 동작할 수 있도록 세팅

 

package com.portfolio;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication(scanBasePackages = "com.portfolio")
@EnableAsync
public class PortfolioWasApplication {
    public static void main(String[] args) {

        SpringApplication.run(PortfolioWasApplication.class, args);
    }
}

 

 

modue-normal-board의 TestController

package com.portfolio.controller;

import com.portfolio.entity.NormalBoard;
import com.portfolio.repo.NormalBoardRepo;
import com.portfolio.service.NormalBoardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@RequestMapping("/test")
@RestController
@RequiredArgsConstructor
@Slf4j
public class TestController {
    private final NormalBoardService normalBoardService;
    @PostMapping("/jpaInsertTest")
    @Tag(name = "jpa insert Test")
    @Operation(summary = "jpa insert Test")
    public String jpaInsertTest() {

        NormalBoard board = NormalBoard.builder()
                .title("testTitle")
                .content("testContent")
                .firstRegTime(LocalDateTime.now())
                .lastUpdTime(LocalDateTime.now())
                .firstRegUser("patrache")
                .lastUpdUser("patrache")
                .build();

        log.info("controller start");
        normalBoardService.insertNormalBoard(board);
        return board.toString();
    }

    @GetMapping("/jpaSelectTest")
    @Tag(name = "jpa select Test")
    @Operation(summary = "jpa select Test")
    public List<NormalBoard> jpaSelectTest() {
        List<NormalBoard> boardList = normalBoardService.selectAllBoardList();
        return boardList;
    }
}

 

 

module-normal-board의 NormalBoardServiceImpl

해당 service는 배치가 아니지만 배치라 가정해고 작성, Async가 동작하는것을 확인하기위해 Thread.sleep으로 테스트

package com.portfolio.serviceimpl;

import com.portfolio.entity.NormalBoard;
import com.portfolio.repo.NormalBoardRepo;
import com.portfolio.service.NormalBoardService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
public class NormalBoardServiceImpl implements NormalBoardService {
    private final NormalBoardRepo boardRepo;
    private Logger logstash = LoggerFactory.getLogger("LOGSTASH");

    @Override
    @Async
    public void insertNormalBoard(NormalBoard board) {
        try {
            Thread.sleep(30000);
            boardRepo.save(board);
        } catch (Exception e) {
            logstash.error("BAT0001||테스트 배치||FAIL||{}", e.toString());
            e.printStackTrace();
        }
        logstash.info("BAT0001||테스트 배치||SUCCESS||ETC");

    }

    @Override
    public List<NormalBoard> selectAllBoardList() {
        return boardRepo.findAll();
    }
}

 

Async 동작 확인

Controller start 로그 30초 이후 Logstash 로그 확인

2024-06-05 16:10:08 INFO  c.p.controller.TestController - controller start
2024-06-05 16:10:09 INFO  c.p.controller.TestController - controller start

2024-06-05 16:10:38 INFO  LOGSTASH - BAT0001||테스트 배치||SUCCESS||ETC
2024-06-05 16:10:39 INFO  LOGSTASH - BAT0001||테스트 배치||SUCCESS||ETC

 


Kibana에서 데이터 확인

http://localhost:5601/
목록 > devtool

 

GET {index}/_search 

 

 


Jenkins 설치 및 배포 

docker run  -p 8088:8080 -p 50000:50000 --restart=on-failure -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk17

 

 

 

 

Ollama 설치

# Ollama docker 이미지 설치
docker pull ollama/ollama

# 컨테이너 생성( ADM 용 + window여서 privalege 옵션추가)
docker run -d --device /dev/kfd --device /dev/dri -v ollama:/root/.ollama -p 11434:11434 --name ollama --privileged ollama/ollama:rocm



 

 


한국어 base모델 설치

https://huggingface.co/heegyu/EEVE-Korean-Instruct-10.8B-v1.0-GGUF/tree/main

 

heegyu/EEVE-Korean-Instruct-10.8B-v1.0-GGUF at main

 

huggingface.co

 

ggml-model-Q4_K_M.gguf 다운로드

# Ollama 내부 접속
docker exec -it ollama bash

# 사용할 디렉토리 생성 
mkdir ~/evee


# 컨테이너 밖에서 작업
# 호스트의 모델을 ollama 컨테이너로 이동
docker cp ./ggml-model-Q4_K_M.gguf ollama:/root/evee


# 컨테이너 내부 접속후
#파일 확인 
/root/evee

 


Custom Model 생성

생성을 위한 모델 설정 파일 생성

Modelfile

FROM ggml-model-Q5_K_M.gguf

TEMPLATE """{{- if .System }}
<s>{{ .System }}</s>
{{- end }}
<s>Human:
{{ .Prompt }}</s>
<s>Assistant:
"""

SYSTEM """A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions."""

PARAMETER stop <s>
PARAMETER stop </s>

 

모델 생성

# 커스텀 모델 생성
ollama create EEVE-Korean-10.8B -f ./Modelfile

# 모델 확인 
ollama list

 


모델 실행 

 

컨테이너 내부에서 사용 

# 컨테이너 내부에서 모델 실행
ollama run EEVE-Korean-10.8B:latest

 

 

컨테이너 외부에서 사용

# 컨테이너 외부에서 모델 실행
docker exec -it ollama ollama run EEVE-Korean-10.8B:latest

 

크롬 확장 프로그램을 활용한 UI

https://chromewebstore.google.com/detail/ollama-ui/cmgdpmlhgjhoadnonobjeekmfcehffco?hl=ko

 

ollama-ui

This extension hosts an ollama-ui web server on localhost

chromewebstore.google.com

 

 

 

 

+ Recent posts