ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 화면 응답 개선하기
    네트워크 & 인프라 2022. 4. 14. 19:31

     

     

     

     

    이번엔 화면응답속도를 개선하는 방법에 대해 알아보자. 

    화면 응답 속도를 개선하는 방법에는 인터넷구간 성능 개선을 통한 방법과 데이터 조회 성능 개선의 방법이 존재한다. 

     

     

     

     

     

    인터넷 구간 성능 개선하기 

    인터넷 구간 성능을 개선하기 위해서는 캐싱설정, CDN 사용, keep-alive 설정, gzip 압축, 이미지 압축, 불필요한 다운로드 제거, 불필요한 작업 지연로딩, 스크립트 병합하여 요청수 최소화, HTTP 프로토콜 개선 등의 방법이 존재한다. 

    • Reverse Proxy 
      • 캐싱설정
      • keep-alive 설정
      • gzip 압축
      • HTTP 프로토콜 개선 (HTTP2를 사용하여 하나의 TCP 연결로 다수의 클라이언트 요청 처리)
    • JS 
      • 불필요한 다운로드 제거 
      • 불필요한 작업을 지연로딩
      • 스크립트 병합하여 요청수 최소화 

     

     

     

    Application Host와 WebServer

     

    출처 NEXTSTEP 프로젝트 공방

    우리는 역할을 분리하기 위해 Web Server와 Application Host를 구성한다. 

    • Tomcat (Application Host) : 실제 비지니스 로직, 복잡한 연산, 동영상 데이터, 데이터베이스 처리 ( scale out이 필요할 수 있음)
    • Nginx (WEB SERVER) : 작은 데이터를 대량 전송, 하드디스크 읽기 쓰기 발생 하지 않을 때 (ex)TLS 암호화, 커넥션 관리, 압축역할 위임, 캐싱(정적리소스)위임, 로드밸런서 역할 (WAS 여러개))
      • Reverse Proxy
      • Load Balancer

     

    1) Servlet  ( Tomcat )

    웹 서버는 클라이언트 요청에 따라 소켓을 생성하여 리소스에 접근한 후 응답을 반환하는데 동적으로 데이터 가공이 필요한 경우 다음과 같이 Servlet 컨테이너에게 요청을 위임한 후 결과를 받아 응답하게 된다. 

    • 사용자가 브라우저를 통해 서버에 HTTP 요청
    • 요청받은 서블릿 컨테이너는 request, response 두 객체를 생성
    • 서블릿 컨테이너는 요청을 처리할 수 있는 서블릿을 찾아 쓰레드를 할당하고 request, response 객체 전달
    • HTTP 메서드에 따라 doGet(), doPost() 메서드 호출
    • 요청결과를 응답한 후 request, response 객체를 제거하고 자원을 반납

    여기서 서블릿 컨테이너는 각 서블릿객체 하나씩만 생성하므로 이곳에 공유 가변필드를 작성하게 되면 Thread safe 하지 않아 요청필드를 건드리면서 blocking 이슈, 동시성 이슈가 발생할 수 있으므로 불변객체를 최대한 사용해야 한다. 

    기존의 queue 보관하였던 방법과 다르게 최근의 서블릿 컨테이너(스프링 NIO)는 쓰레드 수보다 많은 요청이 올 경우 다른 채널에서 대기하다가 콜링 이벤트가 발생하면 처리를 받는데  maxConnection(기본 8192)수까지 연결을 수락한다. 

     

     

    2) Nginx

    Nginx는 queue에 두고 하나씩 처리하는 싱글 프로세스 쓰레드로 비동기 non-blocking 방식으로 처리되어 다수의 커넥션을 처리할 때 용이하며 커넥션만 처리하고 나머지 동작은 스프링에 위임한다. 실제 데이터를 사용하는 것은 별도의 Thread pool을 사용하는데 I/O 시간이 길어지면 성능이 저하된다. 

     

     

    즉 성능 개선 시 Reverse Proxy에서는 클라이언트와의 커넥션 관리, Tomcat에서는 비지니스 로직 개선, 조회 성능개선, 비동기처리, 데이터 캐싱에 집중한다. 

     

     

     

     

     

     

    정적 파일 경량화 

    크롬 브라우저 도구 Network 탭을 사용하여 리소스와 리소스의 속성 (HTTP 헤더, 컨텐츠, 크기 등)을 확인하고 네트워크 대역폭 제한, 브라우저 캐시 비활성화 등을 설정할 수 있다. 정적 파일 경량화는 다음의 방법을 통해 적용할 수 있다. 

    • 번들 크기 줄이기 : 불필요한 라이브러리 제거 또는 용량 작은 라이브러리로 교체
    • Code Splitting
    • Dynamic import
    • 웹 폰트 최적화

     

     

     

     

     

     

    Reverse Proxy 개선

     

    1) gzip 압축 

    gzip 압축을 적용하는 방식에는 Apache나 Nginx등 웹 서버에서 적용하는 방법, Tomcat 등 WAS에서 적용하는 방법, Servlet Filter를 등록하여 처리하는 방법, 정적인 파일을 미리 gzip으로 압축하는 방법이 존재한다. 여기서는 Nginx (Reverse proxy)를 이용해서 gzip을 적용시켜 보자.

    http {
        gzip on; ## http 블록 수준에서 gzip 압축 활성화
        gzip_comp_level 9;
        gzip_vary on;
        gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/rss+xml text/javascript image/svg+xml application/vnd.ms-fontobject application/x-font-ttf font/opentype;
    }

    content-encoding : gzip 압축설정이 된 모습을 확인 가능

     

    2) Cache 

    네트워크 상에서 매번 크기가 큰 응답을 전달하는 것은 많은 데이터 비용을 초래하는데 이전 자원을 재사용 캐싱을 통해 이러한 비용을 최적화 할 수 있다. 

    최초요청 vs 두번째 요청 

    서버는 ETag 을 HTTP 헤더에 담아 유효 토큰으로 통신하고 이를 통해서 효율적인 자원 업데이트 체크가 가능하도록 한다. 또한 각 자원은 HTTP 헤더의 Cache-Control 을 통해서 캐싱 정책이 정의 되는데 이는 응답을 캐시할 수 있는 사용자, 해당 조건, 기간을 제어한다. 

     

     

    'no-cache' , 'no-store'

    Cache-Control: no-cache, no-store
    • no-cache : 요청할 떄마다 ETag를 검사 ( max-age = 0 ) -> 최신상태로 유지 
    • no-store : 캐시를 하지 않음 -> 데이터 유출 방지, 재활용 방지 (ex) 은행 데이터 개인정보))

     

    'public', 'private'

    Cache-Control: private, max-age=600
    • public : 중간 매체를 포함한 모든 서버에 캐시 가능 
    • private : 중간 매체들은 캐시 불가능 (ex)최종 유저의 브라우저는 개인정보 페이지 캐싱 가능, CDN은 불가능) -> 개인정보를 보호한다는 의미는 아님 

     

     

    최적의 Cache-Control 정책

    출처 NEXTSTEP 프로젝트 공방, https://pjh3749.tistory.com/264

    캐싱을 최대한 많이, 오래, 가까이에 하도록 하고 각 자원에 대해 최적화된 캐시 설정을 하는 것이 중요하다. 캐시 주기는 이유가 없다면 1년 정도로 설정한다. 캐시 만료기간 전 자원이 업데이트 된다면 파일의 식별자 혹은 버전 숫자를 파일이름에 삽입하여 리소스의 URL을 바꾸고 컨텐츠가 바뀌었다면 새로 다운로드 하도록 강제할 수 있다. 파일 명을 변경하지 않으려면 캐시 무효화 방식으로 해당 이미지만 업데이트할 수도 있다. 

     

     

    CDN

    CDN(Content Delivery Network)은 여러노드를 지닌 네크워크에 컨텐츠를 저장하여 제공하는 일종의 프록시 기능을 한다. CDN Edge 서버는 리버스 프록시에 비해 사용자와 가까운 곳에 위치하여 컨텐츠 캐시를 전달하여 RTT(Round Trip Time)을 줄여 컨텐츠를 빠르게 전달할 수 있다. 또한 리버스 프록시 처럼 캐시된 컨텐츠를 전송하여 원 서버의 부하를 줄일 수 있고 인프라 확충에 인력과 경비를 줄일 수 있다. Edge 서버간에 캐시를 공유하기 때문에 롱테일 컨텐츠의 경우에도 CDN을 사용하여 캐시 적중률을 높일 수 있다. 

     

    여기서는 css, js, gif등의 자원의 캐시 기간을 한달로 설정하여 저장하였다. 

    http {
      ## Proxy 캐시 파일 경로, 메모리상 점유할 크기, 캐시 유지기간, 전체 캐시의 최대 크기 등 설정
      proxy_cache_path /tmp/nginx levels=1:2 keys_zone=mycache:10m inactive=10m max_size=200M;
    
      ## 캐시를 구분하기 위한 Key 규칙
      proxy_cache_key "$scheme$host$request_uri $cookie_user";
     
      server {
        location ~* \.(?:css|js|gif|png|jpg|jpeg)$ {
            proxy_pass http://app;
      
            ## 캐시 설정 적용 및 헤더에 추가
            # 캐시 존을 설정 (캐시 이름)
            proxy_cache mycache;
            # X-Proxy-Cache 헤더에 HIT, MISS, BYPASS와 같은 캐시 적중 상태정보가 설정
            add_header X-Proxy-Cache $upstream_cache_status;
            # 200 302 코드는 20분간 캐싱
            proxy_cache_valid 200 302 10m;    
            # 만료기간을 1 달로 설정
            expires 1M;
            # access log 를 찍지 않는다.
            access_log off;
        }
      }
    }

    cache 설정 (1M) 이 적용된 것을 확인 가능

     

     

     

    3) 부하분산 

    리버스 프록시에서 부하분산은 서버 설정을 추가해줌으로서 만들 수 있는데 여기서 scale out하여 추가할 서버에서도 보안 규칙에서 포트를 열어 주어야 한다. 

    http {
      upstream app {
        least_conn; ## 현재 connections이 가장 적은 server로 reqeust를 분배
        server 172.17.0.1:8080 max_fails=3 fail_timeout=3s;
        server [scale-out 서버의 public ip]:8081 max_fails=3 fail_timeout=3s;
      }
    
      server {
        location / {
          proxy_pass http://app;
        }
      }
    }

     

     

     

    4) TLS, HTTP/2 설정 

     HTTP2를 사용하여 하나의 TCP 연결로 다수의 클라이언트 요청 처리함으로서 성능을 개선할 수 있다. HTTP 2는 SSL 계층 위에서만 동작한다. 

    http {
      server {
        listen 80;
        return 301 https://$host$request_uri;
      }
      server {  
      listen 443 ssl http2;
        
        ssl_certificate /etc/letsencrypt/live/[도메인주소]/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/[도메인주소]/privkey.pem;
    
        # Disable SSL
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    
        # 통신과정에서 사용할 암호화 알고리즘
        ssl_prefer_server_ciphers on;
        ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
    
        # Enable HSTS
        # client의 browser에게 http로 어떠한 것도 load 하지 말라고 규제합니다.
        # 이를 통해 http에서 https로 redirect 되는 request를 minimize 할 수 있습니다.
        add_header Strict-Transport-Security "max-age=31536000" always;
        
        # SSL sessions
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 10m; 
      }
    }

     

     

     

    모두 적용된 모습은 다음과 같다. 

    events {}
    
    http {
      upstream app {
        least_conn; ## 현재 connections이 가장 적은 server로 reqeust를 분배
        server 172.17.0.1:8080 max_fails=3 fail_timeout=3s;
        server [부하 분산 서버의 public ip]:[부하 분산 서버의 실행 포트] max_fails=3 fail_timeout=3s;
      }
    
      # Redirect all traffic to HTTPS
      server {
        listen 80;
        return 301 https://$host$request_uri;
      }
    
      ## Proxy 캐시 파일 경로, 메모리상 점유할 크기, 캐시 유지기간, 전체 캐시의 최대 크기 등 설정
      proxy_cache_path /tmp/nginx levels=1:2 keys_zone=mycache:10m inactive=10m max_size=200M;
    
      ## 캐시를 구분하기 위한 Key 규칙
      proxy_cache_key "$scheme$host$request_uri $cookie_user";
      
    
      server {
        listen 443 ssl http2;
        ssl off;
        ssl_certificate /etc/letsencrypt/live/[도메인 주소]/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/[도메인 주소]/privkey.pem;
    
        # Disable SSL
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    
        # 통신과정에서 사용할 암호화 알고리즘
        ssl_prefer_server_ciphers on;
        ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
    
        # Enable HSTS
        # client의 browser에게 http로 어떠한 것도 load 하지 말라고 규제합니다.
        # 이를 통해 http에서 https로 redirect 되는 request를 minimize 할 수 있습니다.
        add_header Strict-Transport-Security "max-age=31536000" always;
    
        # SSL sessions
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 10m;
    
      
        gzip on; ## http 블록 수준에서 gzip 압축 활성화
        gzip_comp_level 9;
        gzip_vary on;
        gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/rss+xml text/javascript image/svg+xml application/vnd.ms-fontobject application/x-font-ttf font/opentype;
    
        location / {
          proxy_pass http://app;
        }
    
        location ~* \.(?:css|js|gif|png|jpg|jpeg)$ {
            proxy_pass http://app;
      
            ## 캐시 설정 적용 및 헤더에 추가
            # 캐시 존을 설정 (캐시 이름)
            proxy_cache mycache;
            # X-Proxy-Cache 헤더에 HIT, MISS, BYPASS와 같은 캐시 적중 상태정보가 설정
            add_header X-Proxy-Cache $upstream_cache_status;
            # 200 302 코드는 20분간 캐싱
            proxy_cache_valid 200 302 10m;    
            # 만료기간을 1 달로 설정
            expires 1M;
            # access log 를 찍지 않는다.
            access_log off;
        }
        
      }
    }

     

     

     

     

     

     

     

     

    WAS 성능 개선하기 

    어플리케이션 캐시를 이용해서 기존 작업 결과를 저장하고 동일 작업 요청되었을 때 결과를 재사용하여 성능개선을 할 수 있다. 또한 병렬처리등을 사용하여 제한된 쓰레드 수 안에서 자원을 재사용하여 성능을 개선시킬 수 있다.  

     

     

    1 ) Spring Data Cache

    Redis 를 이용해서 캐시를 이용할 수 있다.  

    ## Redis Server 실행 
    $ docker pull redis
    $ docker run -d -p 6379:6379 redis
    
    ## application.properties에 redis 정보 추가 
    spring.cache.type=redis
    spring.redis.host=localhost
    spring.redis.port=6379
    
    ## build.gradle에 의존성 추가 
    implementation('org.springframework.boot:spring-boot-starter-data-redis')

     

    적용은 다음과 같이 할 수 있다. 

    먼저 CacheConfig를 추가해준다. 

    @EnableCaching
    @Configuration
    public class CacheConfig extends CachingConfigurerSupport {
    
        @Autowired
        RedisConnectionFactory connectionFactory;
    
    
        @Bean
        public CacheManager redisCacheManager() {
            RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    
            RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder.
                fromConnectionFactory(connectionFactory).cacheDefaults(redisCacheConfiguration).build();
            return redisCacheManager;
        }
    }

     

     

     

    다음과 같이 @Cacheable을 이용하여 적용해준다. 여기서 중요한 점은 캐시로 저장할 때 ResponseEntity는 Deserialize 되지 않아 도메인 객체를 직접 반환하거나 Service 계층에서 적용해야 하고 LocalDateTime또한 Deserialize 되지 않으므로 String으로 저장해야하며 AOP의 제약사항을 가져 public method에서만 사용해야 한다는 것이다. 

    @Service
    @Transactional
    public class MapService {
    
        private static final Logger jsonLogger = LoggerFactory.getLogger("json");
    
        @Cacheable(value = "path", key = "{#source, #target}")
        public PathResponse findPath(Long source, Long target) {
            //...
        }
    }
    public class StationResponse {
        private String createdDate;
        private String modifiedDate;
    
        public StationResponse(Long id, String name, LocalDateTime createdDate, LocalDateTime modifiedDate) {
            this.id = id;
            this.name = name;
            this.createdDate = createdDate.format(DateTimeFormatter.ISO_DATE_TIME); //저장시 String 타입으로 저장되도록 변환
            this.modifiedDate = modifiedDate.format(DateTimeFormatter.ISO_DATE_TIME);
        }
    }

     

     

     

     

     

    2) 비동기 처리 

    외부 API의 경우 비동기처리를 통해 병목을 피하고 Thread Pool을 사용하여 쓰레드를 재사용 할 수 있다. 

    • 동기 : 작업을 요청한 후 작업의 결과가 나올 때까지 기다린 후 처리 (프로세스는 커널에 지속적으로 I/O 준비사항을 체크)
    • 비동기 : 직전 시스템 호출의 종료가 발생하면 그에 따른 처리를 진행  
    • Blocking : 유저 프로세스가 시스템 호출을 하고나서 결과가 반환되기까지 다음 처리로 넘어가지 않음
    • Non-Blocking : 호출한 직후에 프로그램으로 제어가 돌아와서 시스템 호출의 종료를 기다리지 않고 다음 처리로 넘어갈 수 있음

     

    비동기 처리 

    비동기 처리는 다음과 같이 적용할 수 있다. 

    @Async
    public void sendMail(String to, String subject, String contents) {

     

     

    Thread Pool

    먼저 적절한 쓰레드 수는 사용 가능한 코어 수 * (1 + 대기 시간/서비스시간)으로 계산할 수 있다.

    ## CPU 모델명
    $ cat /proc/cpuinfo | grep "model name" | uniq -c | awk '{print $5 $6, $7,$8, $9, $10 $11}'
    
    ## CPU당 물리 코어 수
    $ cat /proc/cpuinfo | grep "cpu cores" | tail -1 | awk '{print $4}'
    
    ## 물리 CPU 수 
    $ cat /proc/cpuinfo | grep "physical id" | sort -u | wc -l
    
    ## 리눅스 전체 코어(프로세스)개수 
    $ grep -c processor /proc/cpuinfo
    설정은 TaskExecutionProperties.Pool(Thread pool 생성)에 정의된 설정을 기본으로 따르는데 어플리케이션 구동 상황에 따라 다르게 설정하도록 한다. 
        public static class Pool {
            private int queueCapacity = 2147483647;
            private int coreSize = 8;
            private int maxSize = 2147483647;
            private boolean allowCoreThreadTimeout = true;
            private Duration keepAlive = Duration.ofSeconds(60L);
    @Configuration 
    @EnableAsync 
    public class AsyncThreadConfig { 
    
        @Bean 
        public Executor asyncThreadTaskExecutor() { 
            ThreadPoolTaskExecutor exexcutor = new ThreadPoolTaskExecutor();     
            /* 기본 Thread 사이즈 */
            exexcutor.setCorePoolSize(2); 
            /* 최대 Thread 사이즈 */        
            exexcutor.setMaxPoolSize(4); 
            /* MaxThread가 동작하는 경우 대기하는 Queue 사이즈 */                
            exexcutor.setQueueCapacity(100)
            exexcutor.setThreadNamePrefix("subway-async-"); 
            return exexcutor; 
        } 
    }

     

     

     

     

    (참고한 사이트)

    NextStep 프로젝트 공방 

    https://edu.nextstep.camp/

     

    NEXTSTEP

     

    edu.nextstep.camp

    https://gitabout.com/18

     

    웹사이트 성능 개선을 위한 gzip 압축 적용 : Apache, Tomcat, Weblogic과 Servlet Filter

    유닉스 압축 프로그램 gzip gzip 은 유닉스에서 태어난 오픈소스 압축 프로그램 입니다. 알집이나 7zip 에서 사용하는 것과 동일한 Deflate 압축 알고리즘을 사용하지만 하나의 파일만 압축할 수 있기

    gitabout.com

    https://pjh3749.tistory.com/264

     

    [네트워크] HTTP 캐시(Cache) 정책에 대해서 - 최적의 캐시 전략을 만들자

    다음 내용은 Google의 Web에 관한 칼럼을 번역한 내용이다. 출처는 글 하단에 명시하였다. 최대한 직역을 하려고 했으며 직역이 어색한 경우 괄호안에 옮긴이 표시로 부연설명을 해놓았다. 개요 네

    pjh3749.tistory.com

    https://jay-ji.tistory.com/50

     

    로드밸런싱(Load Balancing) vs 리버스 프록시(Reverse Proxy)

    안녕하세요! 운동하는 개발자 JAY입니다. 오늘은 네트워크에 관련된 내용을 정리해보려고 합니다. 바로 로드밸런싱 과 리버스 프록시 입니다! "로드밸런싱? 그게 뭐지?" 회사에서 일하면서 처음

    jay-ji.tistory.com

     

    https://velog.io/@yeon/redis%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A4%91%EB%B3%B5-%EC%9A%94%EC%B2%AD-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0

     

    redis를 이용한 중복 요청 방지하기

    ITDA 프로젝트는 소비자와 쉽게 접근할 여력이 되지 않는 1차산업 종사자 (판매자)와 소비자를 연결해주는 서비스이다.백엔드단에서 주문하기 기능을 구현한 내용을 정리해보려고 한다.🧐 고민

    velog.io

     

Designed by Tistory.