Sunday 26 July 2020

Intro to Docker - 1

Background

최근 프로젝트를 진행하면서 backend system에서 필요한 다양한 component를 직접 설치해보고 있다. 서버의 지표들을 수집하고 시각화된 dashboard를 제공하는 Prometheus, Grafana, 로그를 수집하여 parsing 및 indexing을 하여 로그에 대한 분석을 제공하는 ELK (ElasticSearch, LogStash, Kibana), 그리고 지속적인 통합과 전달을 제공하는 Jenkins까지 하나씩 설치했다. 각 component의 official site 혹은 개인 blog에서 제공하는 문서를 따라 진행하는데 대부분 guide에서 Docker container 상에서 실행하는 것을 권장하여서 자연스레 Docker를 사용하게 되었다. 이전에 Python 관련 프로젝트를 진행하면서 간단하게 사용한 적은 있었지만 Docker가 성능적인 측면에서 거의 손실이 없다는 점, 분산 시스템에서 부하를 처리하기 위한 전략으로서 탄력적인 scale out을 제공하는 기반 기술이 된다는 점 이 두 측면에 대한 이해도가 이전보다 증가했다고 판단하여 이번 기회에 알아보기로 했다. 

Why Docker? 


Snowflake servers

다수의 서버를 manual하게 운영한다면 다음과 같은 문제들이 발생할 수 있다. application을 실행하는 서버 instance의 환경이 각기 달라서 이후 실행 환경 차이에 의해 application이 제대로 동작하지 않을 수 있다. 겪어본 사람은 알겠지만 서버 환경에 대한 문제를 해결하기 위해서는 어떤 software engineer든 어려움을 겪을 수 있다. 최근에는 cloud 환경에서 개발이 이루어지면서 서버 환경에 대한 차이는 어느 정도 극복하였다. 하지만 application의 상황에 맞추어 복수의 instance에 대해 여러 번 할당/해제한다면 각 instance 간 초기 실행 시점, 중간 종료 시점 등 운영 기록이 달라지게 된다. 즉 instance에 설치한 application의 동작이 다행히 같을지라도 서로 다른 snapshot을 가지게 되는 것이다. 서버 운영 기록을 일종의 암묵지여서 서로 간 공유가 쉽지 않고 개별적으로 관리하여 이 암묵지가 쌓인다면 잠재적 장애의 원인이 될 수도 있다. 각기 다른 서버들 간 눈송이처럼 다른 형상을 띠고 있는 현상을 들어 snowflake server라고 한다.

Infrastructure as a code

위 문제를 해결하기 위해서 infrastrcuture를 code화 하여 관리하기 위한 여러 tool이 도입되었다. Vagrant, Ansible 등이 바로 그것이다. 이 tool들을 활용하면 관리자 역할을 node에서 여러 node에 대해 일관된 서버 운영 (provisioning, configuration management, deployment) 명령을 내릴 수 있게 되고 통합 code로 관리할 수 있게 된다. 하지만 이 방법은 위에서 언급한 snapshot의 차이를 극복하지 못한다.

Docker



> Docker provides the ability to package and run an application in a loosely isolated environment called a container. The isolation and security allow you to run many containers simultaneously on a given host.

Docker는 container의 기반이 되는 기술 (namespace, cgroups)을 활용하여 process에 loosely isolated된 실행 환경을 제공해준다. 아래에서 언급하겠지만 Docker의 실행 단위인 container는 주어진 instance의 환경과 상관없이 Docker image를 기반으로만 실행 환경이 구성되기 때문에 위에서 여러 container들에게 같은 실행 시점을 가지게 할 수 있다. 따라서 software engineer는 Docker를 통해 각기 다른 서버 instance들에 대해서도 완전히 같은 실행 환경 속에서 application을 실행할 수 있다. 또한 기존의 VM과 다르게 host OS와 Docker 실행 환경 사이에 hypervisor, guest OS가 존재하는 것이 아니기 때문에 Linux kernel의 성능을 온전히 활용하기 때문에 성능적인 측면에서도 모자람이 없다.

https://docs.docker.com/get-started/overview/#the-underlying-technology


Image, Container


Image

An image is a read-only template with instructions for creating a Docker container. Often, an image is based on another image, with some additional customization. For example, you may build an image which is based on the ubuntu image, but installs the Apache web server and your application, as well as the configuration details needed to make your application run.

Docker image는 Docker container를 생성하기 위한 instruction (실행 환경, application code 등)을 담고있는 template이다. Dockerfile을 build 하여 생성할 수 있으며 read-only이기 때문에 고정된 실행 환경을 제공해줄 수 있다. 한 image는 다른 image를 생성하기 위한 base가 될 수 있으며 다른 image는 base가 되는 image, 새로이 추가될 binary 및 library, 실행해 필요한 metadata 및 file, 환경 변수 등으로 구성이 되어 새로운 image로 생성이 된다.

Container 

> A container is a runnable instance of an image. You can create, start, stop, move, or delete a container using the Docker API or CLI. You can connect a container to one or more networks, attach storage to it, or even create a new image based on its current state.


Docker container는 image의 실행 가능한 instance이며 host OS에서 돌아가는 하나의 격리된 process이다. 위 그림이 나타내듯이 container는 read-only의 image layers과 read/write가 가능한 container layer로 구성되어 있다. 자체 network interface가 있어 host의 network interface와 연결되어 외부랑 통신하며 persistent data를 저장해야 하는 경우 host의 filesystem 위에서 volume 이나 bind mount을 통해서 저장할 수 있다.


Future Actions 

Docker가 제공하는 강점은 container orchestration 도구와 결합되었을 때 극대화된다. 집단의 container들을 대상으로 service 단위로 구분하여 관리하거나 주어진 traffic에 맞추어 container를 추가 투입하거나 회수하여 효율적으로 resource를 관리할 수 있다. Swarm Classic, Swarm Mode 등이 기존에 사용되었으나 최근에는 Google에서 개발한 Kubernetes가 사실상 orchestration의 표준으로 사용되어 AWS, GCP 등 여러 군데서 사용되고 있다. 다음 시간에는 orchestration의 개발 배경과 개념에 대해 알아보고 관련 tool들도 알아보도록 하겠다. 


References 

Sunday 12 July 2020

Intro to Java cglib

Introduction 


cglib is a powerful, high performance and quality Code Generation Library. It is used to extend JAVA classes and implements interfaces at runtime. See samples and API documentation to learn more about features.
Byte Code Generation Library is high level API to generate and transform JAVA byte code. It is used by AOP, testing, data access frameworks to generate dynamic proxy objects and intercept field access.

Java로 programming을 하는 당신은 적어도 한번은 cglib library를 사용했을 것이다! cglib은 Java bytecode generation library로서 compile time 이후에 class를 조작하거나 생성할 수 있게 한다. bytecode를 추가적으로 장착한다는 맥락에서 bytecode instrumentation library라고도 불린다. cglib을 통해서 대상 class에 대해 dynamic proxy, method intercept가 가능하게 한다. 우리가 널리 사용하는 Spring AOP, Hibernate, Mockito가 cglib을 활용하여 구현되었다. 먼저 간단하게 cglib의 concept를 알아보고 용처에 대해서도 알아보도록 하자. 

Concepts


public class SimpleService {
    
    public String hello(String name) {
        return "Hello~ " + name;    }
    public Integer length(String name) {
        return name.length();    }
}

cglib을 적용할 class는 위와 같다. 대상 class를 subclassing 하기 위해서 cglib의 Enhancer class가 필요하다. Enhancer는 method intercept를 가능하게 하는 dynamic proxy를 생성한다. 

public static SimpleService createFixedValueProxy() {
    Enhancer enhancer = new Enhancer();    enhancer.setSuperclass(SimpleService.class);    enhancer.setCallback((FixedValue) () -> "Hello~ Fixed!");
    return (SimpleService) enhancer.create();}

@Testpublic void testFixedValueProxy() {
    SimpleService proxy = SimpleService.createFixedValueProxy();    assertEquals("Hello~ Fixed!", proxy.hello("abc"));    assertEquals("Hello~ Fixed!", proxy.hello("def"));    assertEquals("Hello~ Fixed!", proxy.hello(anyString()));}

FixedValue는 callback interface로서 proxied method에 대해 고정된 값을 return한다. intercept에 대한 어떤 정보도 제공받지 않는다. 

public static SimpleService createAroundAdviceProxy() {
    Enhancer enhancer = new Enhancer();    enhancer.setSuperclass(SimpleService.class);    enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
        if (method.getDeclaringClass() == Object.class) {
            throw new Exception("declaring class is not a SimpleService");        }
        switch (Method.find(method.getName())) {
            case HELLO:
                return "Hello~ Proxy";            case LENGTH:
                return proxy.invokeSuper(obj, args);            default:
                throw new IllegalArgumentException("cannot find "+ method.getName());        }
    });    return (SimpleService) enhancer.create();}

@Testpublic void testAroundAdviceProxy() {
    SimpleService proxy = SimpleService.createAroundAdviceProxy();    assertEquals("Hello~ Proxy", proxy.hello("hi"));    assertEquals("Hello~ Proxy", proxy.hello("hello"));    assertEquals("Hello~ Proxy", proxy.hello(anyString()));    assertEquals((Integer) 2, proxy.length("hi"));    assertEquals((Integer) 5, proxy.length("hello"));}

cglib을 활용해서 조금 더 동적인 기능을 제공하려면 MethodIntercepter interface를 사용한다. MethodIntercepter는 method, args 등의 정보를 제공 받아서 다양한 처리가 가능하다. 위 code처럼 method signature 별로 처리를 할 수 있다. 

Case Study

Spring AOP

@Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }

@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }

@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }

Spring AOP에서 제공하는 annotation을 통해서 method의 실행 전, 후, exception 처리 후 시점에 대해 추가적인 처리가 가능하다. Spring AOP에서는 AOP 구현을 위해 JDK dynamic proxy 혹은 cglib을 사용한다. JDK dynamic proxy의 사용이 기본적으로 권장되지만 대상 object가 interface를 가지지 않은 경우 cglib이 사용된다. 이에 대한 내용와 code는 아래 link에서 확인할 수 있다. 

https://docs.spring.io/spring/docs/3.0.0.M3/reference/html/ch08s06.html
https://github.com/spring-projects/spring-framework/blob/v5.1.4.RELEASE/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java

@Transactional 


@Transactional
public void businessLogic() {
... use entity manager inside a transaction ...
}
UserTransaction utx = entityManager.getTransaction(); 

try { 
    utx.begin(); 
    businessLogic();
    utx.commit(); 
} catch(Exception ex) { 
    utx.rollback(); 
    throw ex; 
} 

Spring에서 persistent layer에 대한 transaction 처리를 위해 @Transactional annotation을 사용하는데 이때 cglib이 활용된다. code 상의 @Transactional이 아래 code처럼 transaction begin, commit or rollback이 될 수 있도록 bytecode가 generate 된다. 

Mockito

https://proandroiddev.com/mockito-2-x-over-powermock-migration-tips-and-tricks-top-ten-118c52abd1d8

Mockito 2+ 에서는 더이상 cglib으로 구현이 되어있지 않지만, 이전 버전의 Mockito는 cglib을 통해서 구현되었다. Mockito는 대상 class의 method를 아무런 동작을 하지 않는 empty implementation 형태로 확장하였다. 

Conclusion

Java cglib에 대해서 간단히 알아보았다! Java의 bytecode instrumentation 기법 혹은 dynamic proxy이 cglib을 통해서만 구현되어 있는 것은 아니지만 cglib에서 Enhancer class를 통해 subclassing을 하는 기본 원리는 비슷할 것이다 예상한다. 앞으로 마주칠 다양한 AOP 기법이나 testing framework에 대한 이해를 한층 더 높일 수 있을 것으로 기대해본다. 

https://github.com/taehyeok-jang/cglib-example
위 code는 해당 repository에서 확인할 수 있다. 

References 

- https://github.com/cglib/cglib
- https://www.baeldung.com/cglib
- http://cglib.sourceforge.net/apidocs/net/sf/cglib/Enhancer.html
- https://dzone.com/articles/how-does-spring-transactional
- https://docs.spring.io/spring/docs/3.0.0.M3/reference/html/ch08s06.html
- https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-advice

Sunday 5 July 2020

Intro to Java Profiling - 1

Background

규모가 있는 Java application에서는 OS, JVM의 resource를 최대치까지 사용하거나 다수의 thread들 간 race condition이 발생하는 등 시스템에 영향을 주는 일이 발생한다. 이와 같은 일이 발생하기 이전에, 혹은 이전과 동일한 에러로 인해 시스템에서 장애가 발생하지 않도록 시스템 지표들을 분석하고 이를 바탕으로 application을 개선해야 한다. 

JMX


Java에서는 어떻게 성능을 측정하고 그 지표들을 제공해줄까? JMX (Java Management Extension)를 통해 가능하다. JMX는 Java 표준 중 일부로서 application, system objects, devices, networks를 manage하고 monitoring하기 위한 tool을 제공한다. 또한 JVM에 대한 monitoring도 가능하다. 
위 그림이 나타내는 것처럼 JMX는 크게 instrumentation layer, agent layer, distributed service layer 이렇게 three layer로 구성되어 있다. instrumentation layer는 POJO를 wrapping하여 JMX에서 관리하는 MBeans (managed beans) 형태로 만드는 역할을 한다. 이렇게 만들어진 bean은 agent layer에서 creation, registration, deletion 전체 bean lifecycle에 대해서 관리가 된다. agent layer는 MBeans에 대한 control을 하는 부분과 distributed service layer에 대한 API를 제공하는 역할을 한다. Java application 내부에서는 MBeanServer를 실행시킴으로써 동작하며 target MBeans을 이 MBeanServer에등록(registration)할 수 있다. 마지막으로 distributed service layer에서는 local 혹은 remote에 대한 API를 제공하여 MBeans에 대한 operation이나 local cache evict 등의 제어를 할 수 있게 하며 성능 지표를 제공한다. 

https://github.com/taehyeok-jang/jmx-sample

위 링크는 JMX를 사용하는 간단한 code이다. JVM마다 JMX access에 관한 default 설정이 다르므로 실행 환경의 JVM을 살펴볼 필요가 있지만, JVM 실행 시 '-Dcom.sun.management.jmxremote'으로 설정하면 enable하여 JMX access를 가능하게 한다. 다른 설정들로 SSL을 통한 access만 가능하게 하거나, id/password를 사용한 접근만 허용하거나 target application에서 사용하는 listening port와 다른 port를 설정할 수 있게 한다. port를 다르게 하는 것은 특히 deployment 환경에서 중요한데 system 성능 지표를 노출하는 port가 외부로 열려있는 것은 위험하므로, 통상적으로 JMX에 대한 port는 inbound로만 접근이 가능하도록 한다. 

```
java -jar ./build/libs/jmx-sample.jar 
-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port=1617 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false 
```


JConsole을 통해서 위 application의 JMX에 접근을 하면 MBean에 대한 정보를 확인할 수 있고 method를 실행할 수도 있다. 



JConsole, VisualVM 

JMX에서 제공하는 monitoring 기능을 활용하여 Java 성능 profiling을 제공해주는 여러 tool이 있다. 그중에서 대표적인 것이 Java 표준에 속한 JConsole과 OSS로 관리되는 VisualVM이다. 
shell에서 jconsole 명령어를 입력하면 위와 같은 JConsole GUI application이 실행된다. JConsole에서는 JVM에대한 overview, memory, threads, classes 등 시스템에 관한 전반적인 지표를 보여준다. 하지만 다소 지엽적인 형태로 제공해주고만 있는데 조금 더 의미있는 정보를 제공해주는 다른 tool을 살펴보자. 


VisualVM은 본래 Java 표준에 있던 기술이었지만 현재는 OSS로서 계속 개발되고 있으며, JConsole보다 더 다양한 성능 profiling을 제공한다. 



위 그림에서와 같이 시스템 성능 지표를 제공해줄뿐만 아니라 thread dump, heap dump가 가능하며 sampling, profiling을 제공하여 자세한 분석이 가능하다. 아직 sampling, profiling은 제대로 사용하는 방법을 몰라서 소개가 어려운데 아래 영상을 통해서 어떤 기능인지 개괄적으로 살펴볼 수 있다. 


TDA, ThreadLogic 

thread는 Java application의 핵심적인 자원을 하나로서 동작을 항상 주의깊게 살펴보아야 한다. 필요한 최소 개수의 thread로 최대한의 성능을 이끌어내는 것은 중요하다. 실행 중인 application의 thread 동작을 알기 위해서는 thread dump를 떠야하는데 이를 위한 가장 기본적인 방법으로 JDK에 포함된 jstack이 있다. 

```
jstack <target process id> > threaddump



"http-nio-8081-exec-5" #289 daemon prio=5 os_prio=31 cpu=0.10ms elapsed=528.26s tid=0x00007fda88bb0800 nid=0x15303 waiting on condition  [0x0000700012684000]
   java.lang.Thread.State: WAITING (parking)
    at jdk.internal.misc.Unsafe.park(java.base@11.0.7/Native Method)                                       - parking to wait for  <0x000000070897dfc0>
... 
 "http-nio-8081-exec-6" #290 daemon prio=5 os_prio=31 cpu=0.16ms elapsed=528.26s tid=0x00007fda88bb1800 nid=0x15103 waiting on condition  [0x0000700012787000]
   java.lang.Thread.State: WAITING (parking)
```
shell에다가 위와 같이 입력하면 thread dump 정보가 담긴 파일을 얻을 수 있으며 내용을 보면 각 thread (application에서 사용하는 thread, GC thread 등의 system thread, web application의 경우 통신과 관련된 thread 등이 있다)에 대한 정보와 현재 상태 (RUNNABLE, WATING, BLOCKING, ...)를 확인할 수 있다. 그런데 enterprise level의 Java application에서는 수백, 수천개의 thread가 생성될 수도 있는데 이들 각각을 모두 살펴보고 thread들 간의 관계를 파악하는 것은 매우 어렵다. 이를 위한 tool로서 TDA (thread dump analyzer), ThreadLogic 등이 있다. 


위 그림은 ThreadLogic의 실행을 나타낸다. 각 thread에 대한 상태를 간결하게 확인할 수 있으며 공유 자원 (Java monitor lock)등에 접근하는 thread 집합이 어떻게 되는지, 그리고 그 thread들의 자원에 대한 점유율을 확인하여 효율성에 대해서도 분석한다. 위 application은 하나의 동기화된 (synchonized) 공유 자원에 접근하는 10개의 thread를 실행하고 있는데 10개 thread들 중 특정 시점에 해당 자원을 점유할 수 있는 thread는 하나임으로 그 효율이 매우 낮음을 WARN으로 알려주고 있다. 이 정보를 바탕으로 더 나은 Java application 설계가 가능하다.

GCViewer

Java의 메모리 관리는 garbage collector를 통해서 이루어지므로 이에 대한 지표가 필수적이다. Java application을 실행할 때 JVM option으로서 gc log를 남길 수가 있는데 이를 graphical하게 살펴볼 수 있는 tool이 GCViewer이다. GCViewer는 다음에 살펴보도록 하겠다. 


Resources