5.성능 개선하기 - 조회 테스트

Alex·2024년 7월 4일
0

성능 개선

목록 보기
5/9

1)maven을 눌러서 clean-->pakage를 선택하고 jar파일을 만든다.

(테스트를 안하고 하려면 toggle skip test mode를 누른다)

2)jar 파일이 있는 곳에서 shift +우클릭 한 뒤 powershell로 열기를 한다.

(참고로 gradle에서는 ./gradlew build)

3)
scp .\team05-0.0.1-SNAPSHOT.jar root@ip주소:/root/

(비밀번호 입려할 때 마우스 오른쪽으로 해야함)
4)putty를 켜서 host name에다가 ip주소를 붙여넣고 open을 누른다.

5)이 상태에서 putty를 다시 켜서 db를 연다.
--처음에 putty들어갈 때 login as -->이건 아이디 그다음 비밀번호

6)DB에 도커 설치

sudo dnf update
sudo dnf install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install docker-ce docker-ce-cli containerd.io
sudo systemctl start docker
sudo systemctl enable docker

7) MYSQL 실행 커맨드

docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=somepassword -e MYSQL_DATABASE=shortenurl -p 3306:3306 -v /root/mysql-data:/var/lib/mysql -d mysql:latest

docker exec -it mysql-container mysql -u root -p
password는 somepassword

8)

sudo dnf update 패키지 업데이트
sudo dnf install java-17-openjdk-devel
자바 17 설치

sudo update-alternatives --config java 하고서 2누르기
java -jar shortenurlservice-0.0.1-SNAPSHOT.jar

9)8080포트로 방화벽을 열어줘야 한다.
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload

성능 테스트

백그라운드로 실행하기

nohup java -jar shortenurlservice-0.0.1-SNAPSHOT.jar > shortenurlservice.log 2>&1 &

인텔리제이에

create-load-test.yaml 파일 생성

config:
  target: "http://{{ip주소}}:8080"
  phases:
    - duration: 100
      arrivalRate: 10
      rampTo: 100
  payload:
    path: "urls.csv"
    fields:
      - "url"
scenarios:
  - name: "create shortenUrl"
    flow:
      - post:
          url: "/shortenUrl"
          json:
            originalUrl: "{{ url }}"

urls.csv 파일 필요함

1)artillery run create-load-test.yaml -o create-load-report.json

이걸로 테스트 실행

2)artillery report --output create-load-report.html create-load-report.json

보고서 생성

  • tail -f shortenurlservice.log 이렇게 하면 애플리케이션 서버의 로그를 볼 수 있다.

여기서 극단적인 값은 그렇게 중요하지 않다.
중요한 건 p95로 95% 이용자가 얼마나 걸렸는지 확인

3)데이터 db에 넣고서

docker exec -i mysql-container bash -c "rm -f /var/lib/mysql-files/keys.csv && mysql -u root -psomepassword shortenurl -e \"SELECT shorten_url_key FROM shorten_url INTO OUTFILE '/var/lib/mysql-files/keys.csv' FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';\"" && docker cp mysql-container:/var/lib/mysql-files/keys.csv /root/keys.csv

4) 서버에 있는 파일을 가져오기

scp root@{database-server ip주소}:/root/keys.csv .

5)테스트 스크립트 만들기

config:
  target: "http://{ip주소}:8080"
  phases:
    - duration: 100
      arrivalRate: 10
      rampTo: 100
  payload:
    path: "keys.csv"
    fields:
      - "shortenUrlKey"

scenarios:
  - flow:
      - get:
          url: "/{{ shortenUrlKey }}"
          followRedirect: false

6)테스트 실행 artillery run read-load-test.yaml -o read-load-report.json

테스트 결과는 위처럼 나온다.

이미 db인덱스가 걸려 있기 때문에 속도가 상당히 빠르다.
(유니크 제약 조건을 걸면 자동으로 인덱스가 생기기 때문-->유니크한지 아닌지 확인하려면 해당 컬럼들을 한번씩 조회해야하기 때문에)

7) show databases;
SHOW INDEX FROM shorten_url;

uk-->유니크 인덱스

ALTER TABLE shorten_url DROP INDEX {유니크 인덱스 Key_name};
인덱스 제거

인덱스를 제거했더니 엄청나게 느려졌다...
timeout도 상당히 많이 발생했다.

*8080포트 확인
netstat -tuln | grep 8080
ps aux | grep java -->뭔지 확인
kill -9 1732

캐싱 활용하기

package kr.co.shortenurlservice.infrastructure;

import kr.co.shortenurlservice.domain.ShortenUrl;
import kr.co.shortenurlservice.domain.ShortenUrlRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Repository;

import java.util.concurrent.ConcurrentHashMap;

@Repository
public class ShortenUrlRepositoryImpl implements ShortenUrlRepository {

    private final JpaShortenUrlRepository jpaShortenUrlRepository;
    private final ConcurrentHashMap<String, ShortenUrl> cache;

    @Autowired
    public ShortenUrlRepositoryImpl(JpaShortenUrlRepository jpaShortenUrlRepository) {
        this.jpaShortenUrlRepository = jpaShortenUrlRepository;
        this.cache = new ConcurrentHashMap<>();
    }

    @Override
    public void saveShortenUrl(ShortenUrl shortenUrl) {
        jpaShortenUrlRepository.save(shortenUrl);
        cache.put(shortenUrl.getShortenUrlKey(), shortenUrl);
    }

    @Async
    @Override
    public void asyncSaveShortenUrl(ShortenUrl shortenUrl) {
        jpaShortenUrlRepository.save(shortenUrl);
        cache.put(shortenUrl.getShortenUrlKey(), shortenUrl);
    }
    
    //위 메서드들은 캐시 업데이트도 바로한다.

    @Override
    public void increaseRedirectCount(ShortenUrl shortenUrl) {
        jpaShortenUrlRepository.incrementRedirectCount(shortenUrl.getShortenUrlKey());
    }

    @Override
    public ShortenUrl findShortenUrlByShortenUrlKey(String shortenUrlKey) {
        // 먼저 캐시에서 조회
        ShortenUrl shortenUrl = cache.get(shortenUrlKey);

        if (shortenUrl == null) {
            // 캐시에 없으면 데이터베이스에서 조회
            shortenUrl = jpaShortenUrlRepository.findByShortenUrlKey(shortenUrlKey);
            if (shortenUrl != null) {
                // 데이터베이스에 있으면 캐시에 저장
                cache.put(shortenUrlKey, shortenUrl);
            }
        }

        return shortenUrl;
    }

}

처음 테스트랑 그렇게 큰 차이가 없다.

로그를 보면, 캐시가 됐을텐데도 계속 select 쿼리가 나가는 걸 볼 수있다.
jpa의 특성상 update를 하려면 select를 해야 하기 때문에 그렇다.

    @Query("UPDATE ShortenUrl su SET su.redirectCount = su.redirectCount + 1 WHERE su.shortenUrlKey = :shortenUrlKey")
    int incrementRedirectCount(@Param("shortenUrlKey") String shortenUrlKey);
    
      @Transactional(readOnly = false)
    public String getOriginalUrlByShortenUrlKey(String shortenUrlKey) {
        ShortenUrl shortenUrl = shortenUrlRepository.findShortenUrlByShortenUrlKey(shortenUrlKey);

        if(null == shortenUrl)
            throw new NotFoundShortenUrlException();

        shortenUrl.increaseRedirectCount();
        shortenUrlRepository.saveShortenUrl(shortenUrl);

        String originalUrl = shortenUrl.getOriginalUrl();

        return originalUrl;
    }
    
    이걸
    
       @Transactional(readOnly = false)
    public String getOriginalUrlByShortenUrlKey(String shortenUrlKey) {
        ShortenUrl shortenUrl = shortenUrlRepository.findShortenUrlByShortenUrlKey(shortenUrlKey);

        if(null == shortenUrl)
            throw new NotFoundShortenUrlException();

        shortenUrl.increaseRedirectCount();
//        shortenUrlRepository.saveShortenUrl(shortenUrl);
        shortenUrlRepository.increaseRedirectCount(shortenUrl);

        String originalUrl = shortenUrl.getOriginalUrl();

        return originalUrl;
    }
    
    이렇게 변경

처음에는 jpa더티체킹이랑 update 쿼리가 같이 나가서 총 3개의 쿼리가 나감
나중에는 1개만 나감

테스트를 몇번 더 해보면 성능이 좋아질 수 있음
(캐시라서)


테스트 결과를 보면 들쭉날쭉하다
강사님도 성능 테스트를 할 때 직접 확인하면서 좋아지는지 나빠지는지를 계쏙 확인해봐야 한다고 말했다.

레디스 활용

레디스를 쓰면 애플리케이션 내에서만 캐시를 공유하게 된다고 한다. 애플리케이션 간의 캐시 공유를 막는 것

db서버에서 docker run --name redis-container -d -p 6379:6379 redis

application.properties에

spring.redis.host={데이터베이스 ip 주소}
spring.redis.port=6379

추가


@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, ShortenUrl> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, ShortenUrl> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}
@Repository
public class ShortenUrlRepositoryImpl implements ShortenUrlRepository {

    private final JpaShortenUrlRepository jpaShortenUrlRepository;
    private final RedisTemplate<String, ShortenUrl> redisTemplate;
    private static final String CACHE_PREFIX = "shortenUrl::";

//여러 데이터를 넣어줄 수 있기에 프리픽스로 구분함
    @Autowired
    public ShortenUrlRepositoryImpl(JpaShortenUrlRepository jpaShortenUrlRepository,
                                    RedisTemplate<String, ShortenUrl> redisTemplate) {
        this.jpaShortenUrlRepository = jpaShortenUrlRepository;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void saveShortenUrl(ShortenUrl shortenUrl) {
        jpaShortenUrlRepository.save(shortenUrl);
        redisTemplate.opsForValue().set(CACHE_PREFIX + shortenUrl.getShortenUrlKey(), shortenUrl);
    }

    @Async
    @Override
    public void asyncSaveShortenUrl(ShortenUrl shortenUrl) {
        jpaShortenUrlRepository.save(shortenUrl);
        redisTemplate.opsForValue().set(CACHE_PREFIX + shortenUrl.getShortenUrlKey(), shortenUrl);
    }

    @Override
    public void increaseRedirectCount(ShortenUrl shortenUrl) {
        jpaShortenUrlRepository.incrementRedirectCount(shortenUrl.getShortenUrlKey());
    }

    @Override
    public ShortenUrl findShortenUrlByShortenUrlKey(String shortenUrlKey) {
        // 먼저 Redis 캐시에서 조회
        ShortenUrl shortenUrl = redisTemplate.opsForValue().get(CACHE_PREFIX + shortenUrlKey);

        if (shortenUrl == null) {
            // 캐시에 없으면 데이터베이스에서 조회
            shortenUrl = jpaShortenUrlRepository.findByShortenUrlKey(shortenUrlKey);
            if (shortenUrl != null) {
                // 데이터베이스에 있으면 캐시에 저장
                redisTemplate.opsForValue().set(CACHE_PREFIX + shortenUrlKey, shortenUrl);
            }
        }

        return shortenUrl;
    }

}

레디스 쓸려면

sudo firewall-cmd --permanent --add-port=6379/tcp
sudo firewall-cmd --reload

방화벽 열어줘야함

근데, 지금 다시 application.properites값을 읽지 못하는 문제 발생


@Configuration
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String redisHost;

    @Value("${spring.data.redis.port}")
    private int redisPort;

    @Bean
    public RedisConnectionFactory redisConnectionFactory(){
        return new LettuceConnectionFactory(redisHost, redisPort);
    }


    @Bean
    public RedisTemplate<String, ShortenUrl> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, ShortenUrl> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

이렇게 값을 직접 넣어줘야함

(보통 레디스를 쓸 때는 db 서버가 아닌 다른 서버에 놓는다고 한다. 그렇게 하지 않으면, db랑 레디스가 리소스 경합을 할 수 있다고 한다)

나는 저렇게 설정을 바꿔도 안됐는데
db 서버에 방화벽에서 6379 포트를 안 열어주었기 때문이다.

redirect count를 모아서 처리하기

모든 키를 다 지우고 단축 url을 하나로만 만든 뒤에 성능테스트를 해도 반응속도가 그렇게까지 좋지는 않다.

왜냐하면, redirectcount 업데이트를 계속 해야하기 때문이다.
이 경우에는 업데이트를 위해서 레코드에 lock을 걸기 때문.


public interface ShortenUrlRepository {
    void saveShortenUrl(ShortenUrl shortenUrl);
     void asyncSaveShortenUrl(ShortenUrl shortenUrl);
    ShortenUrl findShortenUrlByShortenUrlKey(String shortenUrlKey);
    void increaseRedirectCount(ShortenUrl shortenUrl);


    @Scheduled(fixedRate = 10000)
    void updateRedirectCounts();
}
//10초마다 실행


@Repository
public class ShortenUrlRepositoryImpl implements ShortenUrlRepository {

    private final JpaShortenUrlRepository jpaShortenUrlRepository;
    private final ConcurrentHashMap<String, ShortenUrl> cache;
    private final ConcurrentHashMap<String, AtomicInteger> redirectCountMap;

    @Autowired
    public ShortenUrlRepositoryImpl(JpaShortenUrlRepository jpaShortenUrlRepository) {
        this.jpaShortenUrlRepository = jpaShortenUrlRepository;
        this.cache = new ConcurrentHashMap<>();
        this.redirectCountMap = new ConcurrentHashMap<>();
    }
    @Override
    public void saveShortenUrl(ShortenUrl shortenUrl) {
        jpaShortenUrlRepository.save(shortenUrl);
        cache.put(shortenUrl.getShortenUrlKey(), shortenUrl);
    }

    @Async
    @Override
    public void asyncSaveShortenUrl(ShortenUrl shortenUrl) {
        jpaShortenUrlRepository.save(shortenUrl);
        cache.put(shortenUrl.getShortenUrlKey(), shortenUrl);
    }

    @Override
    public void increaseRedirectCount(ShortenUrl shortenUrl) {
        jpaShortenUrlRepository.incrementRedirectCount(shortenUrl.getShortenUrlKey());
    }

    @Override
    public ShortenUrl findShortenUrlByShortenUrlKey(String shortenUrlKey) {
        // 먼저 캐시에서 조회
        ShortenUrl shortenUrl = cache.get(shortenUrlKey);

        if (shortenUrl == null) {
            // 캐시에 없으면 데이터베이스에서 조회
            shortenUrl = jpaShortenUrlRepository.findByShortenUrlKey(shortenUrlKey);
            if (shortenUrl != null) {
                // 데이터베이스에 있으면 캐시에 저장
                cache.put(shortenUrlKey, shortenUrl);
            }
        }

        return shortenUrl;
    }

    @Scheduled(fixedRate = 10000)
    @Override
    public void updateRedirectCounts() {
        redirectCountMap.forEach((key, count) -> {
            int increment = count.getAndSet(0); //가져오고 키값을 0으로 변경
            if (increment > 0) {
                jpaShortenUrlRepository.incrementRedirectCount(key, increment);
            }
        });
    }

}

리다이렉트를 여러번에 모아서 했을 때 결과다.
이건 내 노트북의 네트워크 통신 문제인거같기도하다...

쿼리는 굉장히 조금 나갔다.

다시 진행해보니 상당히 성능이 괜찮았다.
redirectcount를 카프카에 저장해도 괜찮다고 한다.
특히 유실되면 안되는 데이터는 이렇게 애플리케이션 내부에 저장하는 건 위험하다고 한다. -->카프카가 더 안전

profile
답을 찾기 위해서 노력하는 사람

0개의 댓글