블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

Zipkin을 이용한 MSA 환경에서 분산 트렌젝션의 추적 #2 

 Spring Sleuth를 이용한 Zipkin 연동


조대협 (http://bcho.tistory.com)



앞글에 이어서 이번에는 실제로 어플리케이션에서 분산 로그를 추적해보도록 한다.

스프링 부트 애플리케이션을 Zipkin과 연동하기 위해서는 Sleuth라는 라이브러리를 사용하면 된다.

구조

우리가 구현하고자 하는 예제의 구조는 다음과 같다.


API Client는 User 서비스를 호출하고, User 서비스는 Item 서비스를 호출하여 사용자의 Item 정보를 리턴 받아서 리턴 받은 내용을 API Client에 호출한다.

User와 Item 서비스는 모두 Spring Boot 1.5 버전으로 개발하였다. Spring 2.0은 아직 나온지가 얼마되지 않아서 Zipkin 이 지원되지 않는다.

이 예제에 대한 전체 코드는 https://github.com/bwcho75/zipkin-spring-example 에 있다.

User 서비스 코드

User 서비스 코드를 살펴보도록 하자

maven pom.xml

먼저 maven 빌드 스크립트인 pom.xml에는, zipkin 연동을 위해서 sleuth 라이브러리를 사용하기 위해서 이에 대한 의존성을 추가한다. 아래와 같이 zipkin과 sleuth 라이브러리의 버전은 1.3.2.RELEASE 버전을 사용하였다. 참고로 스프링 부트의 버전은 1.5.5.RELEASE 버전을 사용하였다.


<dependency>

   <groupId>org.springframework.cloud</groupId>

   <artifactId>spring-cloud-starter-zipkin</artifactId>

   <version>1.3.2.RELEASE</version>

</dependency>

<dependency>

   <groupId>org.springframework.cloud</groupId>

   <artifactId>spring-cloud-starter-sleuth</artifactId>

   <version>1.3.2.RELEASE</version>

</dependency>


Controller 클래스

다음은 /users URL을 처리하는  Rest Controller 부분의 코드를 살펴보자, 코드는 다음과 같다.


@RestController

@RequestMapping("/users")

public class UserController {

   @Autowired

   RestTemplate restTemplate;

   

   @Bean

   public RestTemplate getRestTemplate() {

       return new RestTemplate();

   }

   

   @Bean

   public AlwaysSampler alwaysSampler() {

       return new AlwaysSampler();

   }

private static final Logger logger = LoggerFactory.getLogger(UserController.class);

@RequestMapping(value="/{name}",method=RequestMethod.GET)

public List<User> getUsers(@PathVariable String name){

logger.info("User service "+name);

List<User> usersList = new ArrayList<User>();

List<Item> itemList = (List<Item>)restTemplate.exchange("http://localhost:8082/users/"+name+"/items"

,HttpMethod.GET,null

,new ParameterizedTypeReference<List<Item>>() {}).getBody();

usersList.add(new User(name,"myemail@mygoogle.com",itemList));

return usersList;

}


}


getUsers() 함수에서 /users/{name}으로 들어오는 요청을 받아서 RestTemplate을 이용하여 localhost:8082/users/{name}/items로 호출하는 코드이다.

여기서 중요한것이 RestTemplate 객체를 생성하는 방법은데, restTeamplte을 @AutoWrire로 하게 하고, getRestTemplate을 @Bean으로 정해줘야 한다. (아래 문서 참조 내용 참고)


https://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/1.2.1.RELEASE/#_baggage_vs_span_tags

그리고 @Bean으로 정의된 alwaysSampler()를 정의하는데, Sampler란 zipkin으로 트레이싱 하는 트렌젝션을 100%를 다할것인지 일부만 할것인지를 결정하는 것이다. 여기서는 100%를 다하도록 하였다.

100%를 샘플링하면 정확하게 트렌젝션을 추적할 수 있지만, 반대 급부로 매번 샘플링 및 로그를 서버에 전송해야하기 때문에 성능 저하를 유발할 수 있기 때문에 이 비율을 적절하게 조정할 수 있다. 비율 조정은 뒤에 설명할 설정파일에서 조정이 가능하다.

applicaiton.yml

Zipkin 서버의 URL과, 샘플링 비율등을 설정하기 위해서는 src/main/resources/application.yml에 이 설정 정보를 지정해놓는다. 아래는  application.yml 파일이다.


server:

 port: 8081

spring:

 application:

   name: zipkin-demo-server1

 zipkin:

   baseUrl: http://127.0.0.1:9411/

 sleuth:

   enabled: true

   sampler:

     probability: 1.0

sample:

 zipkin:

   enabled: true


port는 이 서비스가 listen할 TCP 포트로 8081로 listen을 하도록 하였다.

spring.zipkin에 baseUrl 부분에 zipkin 서버의 URL을 지정한다. 이 예제에서는 zipkin 서버를 localhost(127.0.0.1):9411 에 기동하였기 때문에 위와 같이 URL을 지정하였다.

다음은 sleuth 활성화를 위해서 spring.sleuth.enabled를 true로 하고 sampler에서 probability를 1.0으로 지정하였다.

Item 서비스 코드

Item 서비스 코드는 User 서비스 코드와 크게 다르지 않다. 전체 코드는 https://github.com/bwcho75/zipkin-spring-example/tree/master/zipkin-service2 를 참고하기 바란다.

Item 서비스는 8082 포트로 기동되도록 설정하였다.

테스트

서비스 개발이 끝났으면 컴파일을 한 후에 User 서비스와 Item  서비스를 기동해보자.

Zipkin 서버 구동

Zipkin 서버를 설치하는 방법은 https://zipkin.io/pages/quickstart 를 참고하면 된다. 도커 이미지를 사용하는 방법등 다양한 방법이 있지만 간단하게 자바 jar 파일을 다운 받은 후에, java -jar로 서버를 구동하는게 간편하다.

wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec'
java -jar zipkin.jar

이때 주의할점은 zipkin 서버를 통해서 HTTP로 Trace 로그를 받을때, 별도의 보안이나 인증 메커니즘이 없기 때문에, zipkin 서버는 반드시 방화벽 안에 놓고, 서비스 서버로부터만 HTTP 호출을 받을 수 있도록 해야 한다.

부하주기

모든 서버가 기동 되었으면 부하를 줘서 로그를 수집해보자. 부하 발생은 간단하게 apache ab 툴을 이용하였다.

%ab -n 1000 http://localhost:8081/users/terry

위의 명령어는  localhost:8081/users/terry로 HTTP GET 요청을 1000번 보내는 명령이다.

결과 확인

부하 발생이 끝난후에 http://localhost:9411 화면으로 들어가서 Find Traces 버튼을 눌러보면 다음과 같은 트레이스 화면을 볼 수 있다. 개개별 트렌젝션 결과가 나오고,


개별 트렌젝션을 눌러보면 다음과 같은 결과가 나오는 것을 볼 수 있다. 아래를 보면 /users/terry가 전체 58.944 ms가 소요되고, users/terry/items는 2 ms가 소요되는 것을 확인할 수 있다. 앞에는 서비스 명인데, 첫번째 서비스는 zipkin-demo-server1, 두번째 서버는 zipkin-demo-server2 로 출력이 된다. 이 서버명은 application.yml 파일에서 지정하면 된다.



재미있는 기능중 하나는 각 서비스의 의존성을 시각화 해주는 기능이 있는데, 화면 위쪽에 dependency 버튼을 누르면 아래 그림과 같이 로그 기반으로하여 서비스간의 호출 의존성을 보여준다.



지금까지 간략하게 Spring Sleuth와 Zipkin을 이용한 분산 로그 추적 기능을 구현해보았다.

여기서 구현한 내용은 어디까지나 튜토리얼 수준이다. Zipkin 서버의 스토리지 구성이 메모리로 되어 있기 때문에 실 운영환경에서는 적합하지 않다. 다음 글에서는 클라우드 환경을 이용하여 운영 수준의 Zipkin 서비스를 구성하는 방법에 대해서 알아보도록 하겠다.


참고 자료

https://howtodoinjava.com/spring/spring-boot/spring-boot-tutorial-with-hello-world-example/

https://howtodoinjava.com/spring/spring-cloud/spring-cloud-zipkin-sleuth-tutorial/



Maven으로 데비안 패키지를 만들어보자


(http://bcho.tistory.com)

조대협

애플리케이션 배포

CI/CD 빌드 배포 프로세스에서, 컴파일된 애플리케이션을 배포하는 방법은 여러가지가 있다. 빌드된 바이너리를 Ansible과 같은 Configuration management 도구를 이용해서 배포하는 방법이 일반적이지만, 작업이 복잡한 경우에는 많은 스크립트 작업이 필요한 경우가 있다.

보통 애플리케이션 배포는 단순하게 바이너리만을 복사하는 것이 아니라, 이에 필요한 의존성이 있는 패키지 (예를 들어 JDK나 기타 의존되는 라이브러리)를 배포해야 하는 경우도 있고, 경우에 따라서는 의존되는 파일이나 복잡한 디렉토리 구조를 생성해야 하는 경우가 있다. 이를 Ansible과 같은 Configuration management 툴을 사용하게 되면 스크립트가 복잡해질 수 있는데, Debian 패키지 (apt-get install 로 설치하는)로 패키징을 하면, 의존성이 있는 패키지나, 복잡한 디렉토리 구조 설정, 그리고 여러 파일 설치를 한번에 끝낼 수 있다.

데비안 패키지 파일 생성

디렉토리 구조

데비안 패키지 생성은 어렵지 않다. 먼저 데비안 패키지 파일에 패키징할 파일을 저장한 “작업 디렉토리"를 하나 만든다. 그리고 그 아래 “홈 디렉토리”에 설치될 경로와 파일을 저장한다.

예를 들어 “홈 디렉토리”가 “/home/terrycho”면 이 패키지를 설치하는 시스템에 /home/terrycho라는 디렉토리가 생성이되고 “작업 디렉토리"아래에 /home/terrycho/my.jar 라는 파일이 있으면 대상 시스템에도 같은 경로에 파일이 설치된다.

파일 구조

패키지에 대한 정보를 설정하기 위해서는 “작업 디렉토리” 아래에 “/DEBIAN” 디렉토리 안에 각종 설정 파일을 넣어둘 수 있는데, control 이라는 파일은 필수로 필요한 파일이다. 이 파일에는 데비안 패키지의 필수 정보인 패키지명, 정보들이 들어간다.  아래는 샘플 내용이다.


Package: ${build.finalName}

Version: ${project.version}

Section: misc

Priority: low

Architecture: all

Depends: oracle-java8-installer | openjdk-8-jre

Description: spinnaker spring test

Maintainer: {my email}


각 항목을 살펴보자

  • Package : 가장 중요한 항목으로 패키지 명을 정의한다. apt-get install 시, 이 패키지명으로 지정해서 인스톨을 한다.

  • Version : 패키지 버전이다. 설치된 패키지를 업그레이드할때 이 버전을 비교하기 때문에 매우 중요한 필드 이다. 버전이 같으면 내용이 다르더라도 업데이트가 되지 않으니 주의가 필요하다.

  • Section : 패키지의 분류인데, 크게 중요하지는 않다.

  • Architecture : 설치 가능한 CPU 플랫폼 종류를 정의한다.

  • Depends : 이 패키지를 실행하기 위해서 필요한 다른 패키지명을 리스팅 한다. 예를 들어 자바 애플리케이션의 경우 JDK를 설치하도록 할 수 있다. 위의 예제는 oracle-java8이나, openjdk-8 런타임을 설치하도록 정의되어 있다.

  • Description : 패키지에 대한 설명을 적는다.

  • Maintainer : 패키지를 관리하는 개발자 이메일을 적는다.


control 파일 이외에도 추가 설정 파일을 통해서 인스톨전 후의 추가 작업을 preinstall이나, postinstall 스크립트를 지정할 수 있다.

패키지를 만들어보자

대략적인 개념을 이해 했으면 실제로 패키지를 만들어보자

아래와 같이 hellodebian 디렉토리에 hello.txt와 control 파일을 생성하였다.


/home/terrycho/hellodebian/home/hello.txt

/home/terrycho/hellodebian/DEBIAN/control


hello.txt 는 간단한 텍스트 내용이 들어있는 파일이고, control 파일의 내용은 다음과 같다.


Package: hellodebian

Version: 1.0

Section: misc

Priority: low

Architecture: all

Depends: oracle-java8-installer | openjdk-8-jre

Description: spinnaker spring test

Maintainer: {my email}


이 디렉토리를 패키지로 묶으려면 “dpkg-deb --build {작업 디렉토리명}” 명령어를 실행하면 된다.



위와 같이 명령을 실행하면 hellodebian.deb 파일이 생성된다.

설치 확인은

% sudo apt-get install ./hellodebian.deb

명령을 실행하면 설치가되는 것을 확인할 수 있고, 의존성에 정의한 Jre가 같이 설치되는것을 확인할 수 있다.




Maven을 이용한 데비안 패키지 파일 생성

그러면 실제 애플리케이션 빌딩 과정에서 이렇게 디렉토리를 구조를 정의하고 dpkg 명령어를 사용해서 패키징을 해야 하는가? 다행이도 언어마다 빌드 스크립트에 데비안 패키지로 패키징을 해줄 수 있는 플러그인이 있다.

여기서는 자바 빌드를 위한 maven에서 데비안 패키지를 묶어주는 jdeb 플러그인에 대해서 알아보자. (https://github.com/tcurdt/jdeb)

아래는 jdeb을 사용하는 pom.xml이다.


   <build>

       <plugins>

           <plugin>

               <groupId>org.springframework.boot</groupId>

               <artifactId>spring-boot-maven-plugin</artifactId>

           </plugin>

           <plugin>

               <artifactId>jdeb</artifactId>

               <groupId>org.vafer</groupId>

               <version>1.5</version>

               <executions>

                   <execution>

                       <phase>package</phase>

                       <goals>

                           <goal>jdeb</goal>

                       </goals>

                       <configuration>

                           <verbose>true</verbose>

                           <snapshotExpand>true</snapshotExpand>

                           <!-- expand "SNAPSHOT" to what is in the "USER" env variable -->

                           <snapshotEnv>USER</snapshotEnv>

                           <verbose>true</verbose>

                           <controlDir>${basedir}/src/deb/control</controlDir>

                           <dataSet>

                    <data>

                       <src>${project.build.directory}/${build.finalName}.jar</src>

                       <type>file</type>

                       <mapper>

                           <type>perm</type>

                           <prefix>/var/${build.finalName}</prefix>

                           <filemode>755</filemode>

                       </mapper>

                   </data>


                           </dataSet>

                       </configuration>

                   </execution>

               </executions>

           </plugin>

       </plugins>

   </build>



<goal>에 jdeb을 설정하고, <configuration>에 상세 설정 정보를 넣는다.

데비안 패키지는 앞에서 설명 했듯이, control 파일이 필요한데, <controlDir>에 control 파일이 저장되어 있는 디렉토리 위치를 지정한다.

다음으로, <dataSet>에 데비안 패키지에 대한 설정 정보를 지정하는데, 디렉토리 정보등을 정의할 수 있다. <type>file</type>은 파일에 대한 정보를 넣는데, <src>에는 소스 파일을 정의하고 <prefix>에 파일이 저장될 위치를 지정한다. <filemode> 에 Unix file permission을 지정한다.

위의 스크립트는 ${prject.build.directory}/${build.finalName}.jar 파일을 /var/${build.finalName}으로 복사한다.


생성된 jar 파일명은 ${build.finalName}으로 생성되는데, 이 이름은

${artifactId}_{$version}_all 이라는 이름으로 된다.

이 예제에서는 artifactId가 hello-springboot 이고, version은 1.0이기 때문에 최종 생성된 이름은

hello-springboot_1.0_all 로 된다.


Bouncy Castle

프로그래밍/LIBS | 2013.03.15 17:00 | Posted by 조대협

http://www.bouncycastle.org/java.html


Java 기반의 암호화 라이브러리

'프로그래밍 > LIBS' 카테고리의 다른 글

Bouncy Castle  (0) 2013.03.15
Fuse 관련  (0) 2011.08.02
XDoclet 간단 예제..  (0) 2007.09.11
무료 차트 API  (0) 2007.08.10

 

내일 오전 5시에(한국시간) Azure 새버전이 발표됩니다.

아마존 서비스에 반격을 하기 위해서, 그리고 이제 개발자나 시장의 상황을 어느정도 인지한 듯한 모양을 보입니다.

기존의 윈도우와 .NET만 지원하던 환경에서

Linux 지원과 Java,Python등의 다른 개발 플랫폼 까지 지원하게 된것이 가장 큰 특징이라고 볼 수 있습니다.

글로벌하게 제대로된 IaaS가 AWS 밖에 없었다면 강력한 경쟁 체재가 생기게 된것입니다.

(이럴줄 알았으면 MS에 계속 있을 걸 그랬습니다.)

 

일단 주목할만한 특징들을 살펴보면

1. IaaS 제공 - Windows Server 뿐만 아니라, CentOS,Ubuntu,Suse Linux 제공

o   Windows Server

§  Windows Server 2008 R2

§  Windows Server 2008 R2 with SQL Server 2012 Eval

§  Windows Server 2012 RC

o   Linux

§  OpenSUSE 12.1

§  CentOS-6.2

§  Ubuntu 12.04

§  SUSE Linux Enterprise Server 11 SP2

2. Azure 서비스에 대해 Python과 Java 지원

Azure의 Queue 서비스, BlobStorage 등등의 기반 서비스들을 접근할 수 있는 Java와 Python SDK를 지원합니다.

 

위의 두가지만으로도 AWS와 어느정도 동등한 수준의 서비스를 제공할 수 있을 것으로 보입니다. 물론 세세한 서비스 기능들은 차이가 있겠지만요

 

3. 새로운 서비스 기능 추가

1) Memcached와 같은 프로토콜을 사용하는 캐쉬 서비스가 추가 되었습니다.

2) Media Service라는 것이 추가 되었는데, 정확한 실체는 분석해봐야 알겠지만, Multimedia Contents에 대한 Streaming CDN이 포함되는 것으로 알고 있습니다. Streaming CDN이란 기존의 CDN이 정적 컨텐츠만 캐슁하여 서비스하는데 반해, 동영상이나 음악을 Streaming해주는 서버를 Edge 서버에 위치 시키고 서비스 해주는 기능을 제공한다.

 기존의 CDN이나 이 Streaming CDN은 Akamai가 주로 제공하는데 상당히 고가이고, AWS의 CDN서비스는 Akamai등에 비해서 Edge Node의 수가 부족하여 충분한 성능 발휘가 어렵고 멀티미디어 컨텐츠를 지원하지 않는 단점이 있었다.

 Microsoft의 경우, 인터넷 회선 보유양이 전세계 3위정도로 상당한 네트워크를 가지고 있기 때문에, 높은 수준의 CDN 서비스를 클라우드 환경에서 제공할 것으로 기대된다.

3) 흥미로운 기능중에 하나가 MongoDB 지원성에 대한 언급이 있는데

"Microsoft also announced the availability of the Eclipse plugin for Java, MongoDB integration, Memcached using non-.NET languages, and code configuration for hosting Solr/Lucene. Developers can find out more in the new Windows Azure Developer Center, which includes additional information, tutorials, samples and application templates to quickly get started and create differentiated cloud scenarios."

실제로 MongoDB를 Azure 안쪽에서 서비스로 위치 시킨것인지 단순하게 SDK만 지원하는지는 열어봐야 겠지만 오픈 소스 NoSQL에 대한 지원을 시작한 점은 고무적이다.

 

결론

이번 업그레이드에서 Linux지원과 Java지원은 방향성은 제대로 잡았다.

그러나 이게 그냥 형식적인 구색갖추기인지, 아니면 본격적인 개방형 클라우드 기술 적용인지는 3~4개월은 지나봐야 알겠지만, 고객입장에서는 AWS이외에 글로벌 서비스 능력을 가진 새로운 IaaS가 생긴것만으로도 선택의 폭은 넓어졌다고 본다.

 

참고 자료

http://www.microsoft.com/en-us/news/download/presskits/cloud/docs/MeetWindowsAzureFS.docx

Azure 런칭 행사 웹사이트 : http://www.meetwindowsazure.com/

 

 

아무래도 전직이 자바 개발자인지라, Azure를 봐도 자바 지원 여부를 보게 되는데, 재미있는 자료를 몇가지 찾아서 첨부한다.


위의 동영상은 Windows Azure Platform 위에서 Tomcat을 구동 시킬 수 있는 방법에 대한 글이다. 이클립스 연동도 되고 대충 쓸만해 보인다. 일반적인 웹 애플리케이션은 그럭저럭 기동 시킬 수 있을 텐데.. 앞뒤에 붙는 Apache Http 서버라던가, Jennifer와 같은 APM은 아마 적용하기 힘들것 같고, Thread Dump를 이용한 Trace라던가 JVM 튜닝 같은 내용의 적용이 쉽지 않아 보인다. 그냥 일반 서비스성 웹애플리케이션이나 이벤트성 서비스에는 그럭저럭 쓸 수 있을 듯 한데, 상용 서비스에서는 글쎄....??

이건 Azure Storage Service들을 Java에서 사용할 수 있는 API SET
http://www.interoperabilitybridges.com/projects/windows-azure-sdk-for-java.aspx
어짜피 Azure는 REST를 지원하니 Java나 PHP,Ruby등에서 당연 사용할 수 있는 것이지만, Library Set을 지원하니 훨씬 개발하기 편한듯 하다. 샘플 코드를 보니 정말 편한데..
아무래도 대용량 데이타를 유지 관리해야 하는 서비스나, BLOB과 같은 데이타를 유지해야 하는 서비스들에 대해서는 충분히 승산이 있을듯.

Amazon EC2에는 AMI로 해서 WebLogic까지 싸악 올릴 수 있군요.
http://www.theserverlabs.com/blog/2009/05/29/setting-up-a-load-balanced-oracle-weblogic-cluster-in-amazon-ec2/
멋집니다. 라이센스를 얼마나 받을 련지 몰겠습니다마나.


Java File Writing 성능 비교

성능과 튜닝/자바 성능팁 | 2009.03.31 17:39 | Posted by 조대협
JAPM을 업그레이드 할까 싶어서 Log Writing 부분을 개선하고자 해서
File Writing을 어떻게 하는 것이 제일 빠를까 테스트를 해봤다.
크게 아래 케이스인데.

1. FileWriter fw = new FileWriter(LOG_HOME+"writer.log");
2. BufferedWriter bw = new BufferedWriter(new FileWriter(LOG_HOME+"writer.log"));
3. FileOutputStream fos = new FileOutputStream(LOG_HOME+"outputstream.log");
4. BufferedOutputStream fos =  new BufferedOutputStream(new FileOutputStream(LOG_HOME+"bufferedoutputstream.log"));
5. FileChannel fc =(new FileOutputStream(new File(LOG_HOME+"filechannel.log"))).getChannel(); + Byte Buffer 매번 생성
6. FileChannel fc =(new FileOutputStream(new File(LOG_HOME+"filechannel.log"))).getChannel(); + ByteBuffer 재사용

테스트의 정확성을 위해서 측정중에 GC가 발생하지 않도록 NewSize와 HeapSize를 크게 해놓고 테스트를 하였다. Vm 옵션은 -XX:NewSize=480m -ms768m -mx768m -verbosegc 이다.
환경 : Windows XP, Sun JVM 1.5, Centrio VPro Dual core, IBM Thinkpad X61s

결과는 다음과 같다.
   1K  2K  5K  10K  50K  
 FileWriter  31  32  94 203  1281  
 FileWriter + BufferedWriter  15  31  94  188  1000  
 FileOutputStream  32 47  109  188  1063  
 FileOutputStream + BufferedOutputStream  31  47  109  203  1578  
FileChannel  47  63  109  219  2906  
FileChannel + Byte Buffer 재사용 31 47 188 250 2766

(해당 레코드를 1000번씩 write)

예상하기로는 NIO의 FileChannel이 가장 빠를것이라 생각했는데, 의외로 FileWrite와 FileOutputStream의 성능이 높게 나왔다. NIO의 경우 JVM 벤더에 종속성이 있겠지만 이런 결과가 나오는것은 약간 의외였다.
 오히려 프로그래밍 기법을 내서 FileChannel을 사용할 경우 최대 3배까지 성능이 나쁜 경우가 올 수 있으니. 이런건 차라리 모르는게 나을 수 도 있겠다~~ 싶다.. ^^

BufferedWriter등의 경우 GC를 유발하는 문제가 있을 수 있기 때문에 또 다른 성능 저하 요인이 될 수 는 있겠지만, 현재까지 테스트 결과로는 Windows Platform에서는 FileWriter + BufferedWriter의 경우가 성능이 제일 좋은것 같다.
아래는 테스트에 사용한 소스 코드
==
package bcho.filewriter.test;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import junit.framework.TestCase;

public class FileWriterTest extends TestCase {
final static int loop = 1000;
final static String LOG_HOME="c:/temp/";
static String LOG_STRING="";
static {
}
public void testSuite() throws Exception{
int stringSize[]={100,200,500,1000,5000};
for(int i=0;i<stringSize.length;i++){
LOG_STRING="";
for(int j=0;j<stringSize[i];j++) LOG_STRING+="1234567890";
log(stringSize[i]+"0 bytes");
testFileWriter();
System.gc();
testBufferedWriter();
System.gc();
testFileOutputStream();
System.gc();
testFileBufferedOutputStream();
System.gc();
testFileChannel();
System.gc();
testFileChannelOneBuffer();
System.gc();
}
}
public  static void log(String str){
System.out.println(str);
}
// java.io.FileWriter
private void testFileWriter() throws Exception {
FileWriter fw = new FileWriter(LOG_HOME+"writer.log");
long st = Timer.getCurrentTime();
for(int i =0;i<loop;i++){
fw.write(LOG_STRING);
}
log("FileWriter :"+Timer.getElapsedTime(st) +"ms");
fw.close();
}
// java.io.BufferedWriter
private void testBufferedWriter() throws Exception {
BufferedWriter bw = new BufferedWriter(new FileWriter(LOG_HOME+"writer.log"));
long st = Timer.getCurrentTime();
for(int i =0;i<loop;i++){
bw.write(LOG_STRING);
}
log("BufferedWriter :"+Timer.getElapsedTime(st) +"ms");
bw.close();
}
// java.io.FileOutputStream
private void testFileOutputStream() throws Exception{
FileOutputStream fos = new FileOutputStream(LOG_HOME+"outputstream.log");
long st = Timer.getCurrentTime();
for(int i=0;i<loop;i++){
byte[] buf = LOG_STRING.getBytes();
fos.write(buf);
}
log("FileOutputStream :"+Timer.getElapsedTime(st) +"ms");
fos.close();
}
// java.io.FileOutputStream
// + java.io.BufferedOutputStream
private void testFileBufferedOutputStream() throws Exception{
BufferedOutputStream fos = 
new BufferedOutputStream(
new FileOutputStream(LOG_HOME+"bufferedoutputstream.log"));
long st = Timer.getCurrentTime();
for(int i=0;i<loop;i++){
byte[] buf = LOG_STRING.getBytes();
fos.write(buf);
}
log("FileBufferedOutputStream :"+Timer.getElapsedTime(st) +"ms");
fos.close();
}
private void testFileChannel() throws Exception {
FileChannel fc =(new FileOutputStream(new File(LOG_HOME+"filechannel.log"))).getChannel();
long st = Timer.getCurrentTime();
for(int i=0;i<loop;i++){
byte[] buf = LOG_STRING.getBytes();
ByteBuffer bytebuffer = ByteBuffer.allocate(buf.length);
bytebuffer.put(buf);
bytebuffer.flip();
fc.write(bytebuffer);
}
log("FileChannel  :"+Timer.getElapsedTime(st) +"ms");
fc.close();
}
private void testFileChannelOneBuffer() throws Exception {
FileChannel fc =(new FileOutputStream(new File(LOG_HOME+"filechannelonebuf.log"))).getChannel();
int BUF_SIZE=1000;
long st = Timer.getCurrentTime();
ByteBuffer bytebuffer = ByteBuffer.allocate(BUF_SIZE);
for(int i=0;i<loop;i++){
byte[] buf = LOG_STRING.getBytes();
int offset=0;
int length= buf.length;
while(offset < length){
int chunkSize = BUF_SIZE > length-offset ? length-offset-1 : BUF_SIZE;
bytebuffer.put(buf, offset, chunkSize);
bytebuffer.flip();
offset+=BUF_SIZE;
fc.write(bytebuffer);
bytebuffer.clear();

}
}
log("FileChannel with reusing buffer:"+Timer.getElapsedTime(st) +"ms");
fc.close();
}


}


'성능과 튜닝 > 자바 성능팁' 카테고리의 다른 글

Java File Writing 성능 비교  (8) 2009.03.31

EasyMock을 이용한 단위 테스트

ALM/Test Automation | 2008.11.07 18:06 | Posted by 조대협

 

Unit Test with Easy Mock

자바스터디 조대협(bwcho75@지메일.컴)

 단위 테스트는 소프트웨어 구성 요소의 각 컴포넌트를 독립된 환경에서 테스트 하는 것이다. 그렇지만 일반적으로 소프트웨어 컴포넌트는 혼자서 동작할 수 없고 다른 컴포넌트에 대해서 종속성(Dependency)를 가지고 있기 때문에 종속관계에 있는 컴포넌트가 완성되지 않거나 그 컴포넌트에 오류가 있으면 정상적으로 테스트를 진행할 수 없다.

이 문서를 읽기 전에 먼저 Junit 테스트에 대해서 숙지하기 바란다.
http://bcho.tistory.com/entry/단위-테스트-1회-JUnit

이런 문제를 해결하기 위해서 사용하는 것이 Mock Object 이다. Mock Object는 가상 오브젝트로 테스트를 위한 Operation만을 구현하여 테스트에 사용할 수 있다. 

이러한 Mock Object를 POJO (Plain Old Java Object)로 만들어서 직접 구현할 수 있지만 이러한 경우 모든 Interface를 구현해야 하고, 테스트 케이스가 해당 Mock Object에 대해서 종속성을 가지게 되며, 시나리오에 따라서 Mock Object를 따로 작성해야 하기 때문에 효율성이 떨어진다. 

이러한 문제를 해결하기 위해서 고안된 프레임웍이 EasyMock이라는 테스팅 프레임웍이다. 이 프레임웍은 Junit 3.8X와 4.X와 함께 사용되어 단위테스트에서 Mock Object 생성을 지원한다. 내부적으로 EasyMock은 Java의 Reflection을 이용하여 단위테스트 Runtime에서 가상 객체와 그 객체의 메서드를 생성하여 준다. 

1.       다운 로드 받기

http://www.easymock.org에서 easymock 2.4 를 다운로드 받는다.압축을 풀면 easymock.jar가 나온다. 이 파일을 이클립스 프로젝트내에 클래스 패스에 추가한다.

2.       클래스 작성

작성할 클래스는 RunCaculator라는 클래스로 두개의 메서드를 가지고 있다.

Ø         doSum 메서드는 Caculator라는 클래스를 호출하여 a,b 두개의 값을 더해서 리턴을 한다.

Ø         sayHello  메서드는 Caculator 클래스를 호출하여 입력받은 문자열을 출력한다.

package bcho.easymock.sample;

 

public class RunCaculator {

        Caculator cal;       

        public void setCal(Caculator cal) {

               this.cal = cal;

        }

         // return sum of a and b

        public int doSum(int a,int b){

              System.out.println("## summing "+a+"+"+b+"="+ cal.sum(a, b) ); 

               return cal.sum(a, b);

        }

        // echo string to console

        public void sayHello(String str){

               cal.echo(str);

        }

}

 3.       종속된 인터페이스 작성

Caculator 인터페이스는 두개의 메서드를 가지고 있다.

package bcho.easymock.sample;

public interface Caculator {

        public int sum(int a,int b);

        public void echo(String echo);

}

 Ø         A,B 두개의 값을 더해서 리턴하는 sum 메서드와

Ø         입력 받은 문자열을 화면으로 출력하는 echo 라는 메서드

4.       테스트 케이스 작성

테스트를 하고자 하는 클래스 RunCaculator는 이미 완성되어 있다. 그러나 이 클래스가 사용하는 인터페이스 Caculator에 대한 Implement Class가 없다. 이 Caculator에 대한 클래스를 EasyMock을 이용해서 Simulation 해볼것이다.

Mock Object의 사용 순서는 간단하다.

Ø         Mock Object를 생성한다.

Ø         Mock Object가 해야 하는 행동을 녹화한다. (record)

Ø         Mock Object의 행동을 수행하도록 한다. (replay)

Ø         테스트를 수행한다.

예를 통해서 살펴보자.
EasyMock 을 사용하기 위해서 easymock 정적 메서드들을 아래와 같이 Import 한다.

import static org.easymock.EasyMock.aryEq;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;

 테스트 케이스의 Set up에서 테스트를 할 객체를 생성하고 Mock object를 createMock 메서드를 이용해서 생성한다. 이때 인자는 생성하고자 하는 가상 객체의 클래스명(인터페이스명)을 주면 된다. 

public class CaculatorTest extends TestCase {

        Caculator mock;
        RunCaculator runner;
        protected void setUp() throws Exception {

               mock = createMock(Caculator.class);   // create Mock Object
               runner = new RunCaculator();
               runner.setCal(mock);
               super.setUp();
        }

 

먼저 Mock Object의 행동을 recording해야 하는데,

mock.메서드

로 정의하면 해당 행동이 recording 된다. 만약에 리턴값이 있을때에는

expect(mock.메서드).andReturn(리턴값)

식으로 정의하면된다.

예제를 보고 정리하면 앞으로 불릴 Mock object는 sum(1,2)라는 메서드가 호출될것이고 그에 대해서 리턴값을 3을 리턴한것이다. 라고 정의하는 것이다. sum(1,2)가 아닌 다른 인자로 호출이 되면 미리 레코딩 된 행동이 아니기 때문에 에러가 날것이고 마찬가지로 sum(1,2)가 호출되더라도 1회 초과로 호출되면 이 역시 예상된 행동이 아니기 때문에 에러가 발생된다.

 

        public void testDoSum() {
               expect(mock.sum(1,2)).andReturn(3);   // record mock action
               replay(mock);                         // replay mock          

               this.assertEquals(3,runner.doSum(1,2));              

               verify(mock);
        }

 아래의 예는 일부로 에러를 발생 시킨 경우인데 echo(Hello) 메서드가 한번만 호출되기로 되어 있었는데, 아래 테스트 케이스를 보면 echo(Hello)가 연속으로 두번 호출 된후 echo(Hello bcho)가 한번 호출되기 때문에 에러가 발생된다.

        public void testSayHello() {
               mock.echo("Hello");
               replay(mock);
               runner.sayHello("Hello");
               runner.sayHello("Hello");
               runner.sayHello("Hello bcho");
               verify(mock);

       }

 


 

 

Thread Pool이라는 것이 얼핏 생각하면 구현이 간단할 수 도 있지만, 막상 구현하려면 상당히 귀찮은데, JDK 1.5에 이 내용이 포함되어 있다.
상당히 설계도 잘한것 같아서 마음에 드는데..
이정도면 쉽게 웹서버정도는 만들 수 있지 않을까?

http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ThreadPoolExecutor.html#execute(java.lang.Runnable)

생성자를 잠깐 살펴보면

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

* corePoolSize : Pool의 MIN크기
* maximumPoolSize : Pool의 MAX 크기
* keepAliveTime : Thread가 Idle이고 현재 전체 쓰레드수가 corePoolSize보다 많을때, 이 시간동안 Idle한 쓰레드는 없어진다. (Thread shirinking)
* keepAliveTime 단위
* workQueue : 쓰레드 풀에 대한 큐.
큐잉을 안하는 방법도 있고,
여러가지 정책이 있는데
Queuing
Any BlockingQueue may be used to transfer and hold submitted tasks. The use of this queue interacts with pool sizing:
  • If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
  • If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
  • If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.
There are three general strategies for queuing:
  1. Direct handoffs. A good default choice for a work queue is a SynchronousQueue that hands off tasks to threads without otherwise holding them. Here, an attempt to queue a task will fail if no threads are immediately available to run it, so a new thread will be constructed. This policy avoids lockups when handling sets of requests that might have internal dependencies. Direct handoffs generally require unbounded maximumPoolSizes to avoid rejection of new submitted tasks. This in turn admits the possibility of unbounded thread growth when commands continue to arrive on average faster than they can be processed.
  2. Unbounded queues. Using an unbounded queue (for example a LinkedBlockingQueue without a predefined capacity) will cause new tasks to be queued in cases where all corePoolSize threads are busy. Thus, no more than corePoolSize threads will ever be created. (And the value of the maximumPoolSize therefore doesn't have any effect.) This may be appropriate when each task is completely independent of others, so tasks cannot affect each others execution; for example, in a web page server. While this style of queuing can be useful in smoothing out transient bursts of requests, it admits the possibility of unbounded work queue growth when commands continue to arrive on average faster than they can be processed.
  3. Bounded queues. A bounded queue (for example, an ArrayBlockingQueue) helps prevent resource exhaustion when used with finite maximumPoolSizes, but can be more difficult to tune and control. Queue sizes and maximum pool sizes may be traded off for each other: Using large queues and small pools minimizes CPU usage, OS resources, and context-switching overhead, but can lead to artificially low throughput. If tasks frequently block (for example if they are I/O bound), a system may be able to schedule time for more threads than you otherwise allow. Use of small queues generally requires larger pool sizes, which keeps CPUs busier but may encounter unacceptable scheduling overhead, which also decreases throughput.

일반적으로 LinkedBlockingQueue를 사용하면 문제는 없을듯하고, Queue의 MAX길이를 지정하고, QueueFull 시에 충분한 Exception 처리를 해주는것이 좋을듯하고
- threadFactory : 풀 크기를 느릴때 생성되는 쓰레드를 생성하는 팩토리
- handler : 풀이나 큐가 차서 처리를 못할때, Reject에 대한 처리를 하는 Handler

실제 Q에서 작업을 꺼내와서 수행하는 방법은
execute(Runnable r)을 사용하면 되고

흥미로운것은 (Hooking)
execute 전후에 훅킹 메서드를 beforeExecute,afterExecute를 넣을 수 있다는 것

'프로그래밍 > 프로그래밍팁' 카테고리의 다른 글

Work load manager in JEE  (0) 2008.02.29
NIO  (0) 2008.02.27
JDK 1.5 부터 등장한 ThreadPool  (0) 2008.02.27
SQL Batch  (0) 2007.11.28
대용량 Record select  (0) 2007.11.28
Java Application의 Locking 처리문제  (0) 2007.08.21

진짜 별게 다 나온다.
오픈 소스가 많이 발전하고,
이제 J2EE 급의 각종 오픈소스 프레임웍들이 나오더니..
이런것에 대한 통합과 정리의 필요성을 생각하고 있었는데.
실제로 Spring의 경우는 오픈소스들의 컨테이너와 같은 역할을 하면서 수많은 커넥터 들을 만들어 내고 있었다.
그런데 왠걸? SourceForge에서 EJSOA로 Enterprise용 Java Open Source 아키텍쳐를 내 놓았다.

사용자 삽입 이미지

얼마나 실용적일까는 두고봐야할 일이지만, 상용 J2EE 벤더 입장에서는 그리 반갑지 않은 오픈소스가 아닐까 싶다. 이대로 가다가는 상용벤더들은 Middleware보다는 솔루션과 컨설팅등에 집중해야 하지 않을까?
다음은 EJOSA 관련 자료들에 대한 링크
http://blog.naver.com/comsnake?Redirect=Log&logNo=80005937833
http://blog.naver.com/comsnake?Redirect=Log&logNo=80005937833

'IT 이야기 > 트렌드' 카테고리의 다른 글

자바 기술 트렌드 분석 - 2. OR Mapping  (1) 2009.04.30
자바 기술 트렌드 분석 - 1. MVC  (1) 2009.04.30
구글  (0) 2007.11.21
요즘 개발의 트렌드  (0) 2007.09.04
EJOSA (Enterprise Java Open Source Architecture)  (0) 2007.08.27
기술 기술..  (0) 2007.07.25
Java AP에서 Locking처리 방법은 Synchronized 를 사용하는 방법이 대표적인데
이 경우에는 하나의 JVM Instance 내에서만 동기화 처리가 가능하다.

시스템을 설계할때, 다중 인스턴스 구조의 부하 분산 환경을 고려한다면, 인스턴스내의 Locking 처리인지 아니면 인스턴스간의 Locking처리가 필요한지를 먼저 결정해야 하고, 인스턴스간의 Locking처리인 경우에는 DB나 아니면 기타 (RMI,JMS등) 방법을 사용하는 방식이 있다.

특히 DB의 Lock 처리 메커니즘을 생각할때 고려할 부분은
보통 다음과 같은 구조로 만드는 경우가 많다.
1: select LOCK
2: if( unlocked){
3: update set LOCK
4: }else { return "Lock이 걸려있음"}
5: 임계구역

그러나 이경우, 두개의 인스턴스 A,B가 있다고 했을때
A가 1을 수행하고 Lock이 없어서 Lock을 잡으려고 시도해서 2라인에 진입했다.
이때 B가 Lock체크를 하기 위해 1에 진입하였다면?
A가 아직 Lock을 잡기 전이기 때문에, 결과적으로 A,B 두개 모두 임계구역에 진입하게 된다. Single Instance인 경우에는 당연히 Synchronized처리로 해결하면 되겠지만,
Multi Instance 인경우 다음과 같은 방안이 있다.

1) select하기 전에 해당 row에 DBLock을 거는 방법
2) select하지 않고 update의 return값으로 체크하는 방법

1) 방안은 DB마다 다르기 때문에 별도로 설명하지 않고
2)의 방안을 보면
1: ret = stmt.exeucteUpdate("UPDATE LOCK");
2: if(ret == 0){ return "Lock이 걸려있음"}
3: 임계구역

stmt.executeUpdate는 update에서 반영된 레코드수를 리턴하기 때문에 이미 lock에 대한 update가 된경우에는 0을 리턴한다.
※ 그러나 JDBC 드라이버에 따라서 executeUpdate에 대해서 update에 대한 리턴값이 무조건 0인경우가 있을 수 있기 때문에 반영전에 테스트를 필요로 한다.