public static String StringReplace(String str){
String match = "[^\uAC00-\uD7A3xfe0-9a-zA-Z\\s]";
str =str.replaceAll(match, " ");
return str;
}
public static boolean isEmailPattern(String email){
Pattern pattern=Pattern.compile("\\w+[@]\\w+\\.\\w+");
Matcher match=pattern.matcher(email);
return match.find();
}
public static String continueSpaceRemove(String str){
String match2 = "\\s{2,}";
str = str.replaceAll(match2, " ");
return str;
}
logback은 SLF4J의 native 구현체 입니다. slf4j로 어플리케이션 로그를 남긴다면 logback을 선택하는게 가장 좋습니다. slf4j의 도움으로 연관 라이브러리들이 다른 logging framework를 쓰더라도 logback으로 통합할 수 있습니다.
logback 은 logback-core, logback-classic, logback-access의 3개의 모듈이 있습니다. core는 classic과 access의 공통라이브러리입니다. maven repository를 쓴다면 classic만 추가하면 관련 라이브러리가 추가 됩니다.
Maven pom.xml
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
External Libraries
Maven: ch.qos.logback:logback-classic:1.1.2
Maven: ch.qos.logback:logback-core:1.1.2
Maven: org.slf4j:slf4j-api:1.7.6
logback을 사용할수 있는 준비가 끝났습니다. 아래 코드를 바로 실행해봅시다.
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.util.StatusPrinter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Tutorial {
private static final Logger logger = LoggerFactory.getLogger(Tutorial.class);
public static void main(String[] args) {
logger.trace("trace");
logger.debug("debug");
logger.info("info");
logger.warn("warn");
logger.error("error");
}
}
Logger와 LoggerFactory는 SLF4J에 있는 interface와 implements입니다. 실제 어플리케이션상에 logger를 정의 할때 logback 관련 코드를 넣지는 않습니다. 다시 말해 logback과 dependency가 없는 코드를 구현하게 되면 차후 다른 logging framework로 교체하는게 가능하게 됩니다.
Logger logger = LoggerFactory.getLogger(Tutorial.class);
20:57:59.346 [main] DEBUG example.logback.Tutorial - debug
20:57:59.350 [main] INFO example.logback.Tutorial - info
20:57:59.350 [main] WARN example.logback.Tutorial - warn
20:57:59.350 [main] ERROR example.logback.Tutorial - error
java에서는 GC가 있어 리소스를 관리해준다. 하지만 외부 리소르를 클린업을 하지 않아 오작동을 발생하는 실수를 많이 하게 된다.
단계별로 외부리소를 클린업을 하는 법을 정리하고, 최종적으로 람다표현식을 사용한 리소스클린업을 정리한다.
public class FileWriterEx {
private final FileWriter writer;
public FileWriterEx(final String fileName) throws IOException {
writer = new FileWriter(fileName);
}
public void writeStuff(final String message) throws IOException {
writer.write(message);
}
public void finalize() throws IOException {
writer.close();
}
public static void main(String[] args) throws IOException {
final FileWriterEx writerEx = new FileWriterEx("joswlvTest.txt");
writerEx.writeStuff("TEST-TEST");
}
}
이런 경우 joswlvTest.txt
파일은 생성되지만 내용은 비어 잇다. finalize()
가 실행되지 않았고, JVM은 충분한 메모리가 있기 때문에 finalize()
를 할 필요가 없다고 생각한다. 그래서 파일은 종료되지 않고 작성한 내용은 메모리에서 파일로 flush가 되지 않았다.
흔히하는 실수
문제를 수정하기 위해 명시적으로 close()
호출을 추가하고 finalize()
메소드를 제거한다.
인스턴스 사용이 끝나면, 해당 인스턴스를 클린업할 것을 명시적으로 요청할 수 있게 한것이다.
public class FileWriterEx {
private final FileWriter writer;
public FileWriterEx(final String fileName) throws IOException {
writer = new FileWriter(fileName);
}
public void writeStuff(final String message) throws IOException {
writer.write(message);
}
public void close() throws IOException {
writer.close();
}
public static void main(String[] args) throws IOException {
final FileWriterEx writerEx = new FileWriterEx("joswlvTest.txt");
writerEx.writeStuff("TEST-TEST");
writerEx.close();
}
}
close()
를 명시적으로 호출하는 것은 인스턴스가 사용한 외부 리소스를 클린업한다. 그러나 코드에서 예외처리가 일어나면 close()
메소드가 호출하지 못할 수도 있다.
close()
메소드 호출을 보자하는 수정을 해보자
public class FileWriterEx {
private final FileWriter writer;
public FileWriterEx(final String fileName) throws IOException {
writer = new FileWriter(fileName);
}
public void writeStuff(final String message) throws IOException {
writer.write(message);
}
public void close() throws IOException {
writer.close();
}
public static void main(String[] args) throws IOException {
final FileWriterEx writerEx = new FileWriterEx("joswlvTest.txt");
try {
writerEx.writeStuff("TEST-TEST");
} finally {
writerEx.close();
}
}
}
이렇게 하면 코드에서 예외가 발생하더라도 리소스에 대한 클린업이 보장된다.
java SE7에서 부터 제공하는 ARM (Automatic Resource Management)
기능을 사용해 장황한 코드를 감소 시켜 보자!
public class FileWriterEx implements AutoCloseable{
private final FileWriter writer;
public FileWriterEx(final String fileName) throws IOException {
writer = new FileWriter(fileName);
}
public void writeStuff(final String message) throws IOException {
writer.write(message);
}
public void close() throws IOException {
System.out.println("Close호출됨!");
writer.close();
}
public static void main(String[] args) throws IOException {
try (FileWriterEx writer = new FileWriterEx("joswlv.txt")) {
writer.writeStuff("TEST-TEST");
}
}
}
클래스의 인스턴스를 try-with-resources
형태로 생성하고 그블록 안에서 writeStuff()를 호출한다. try 블록을 빠져나오면, 자동적으로 try 블록에 의해 관리되는 인스턴스/리소스에서 close()
메소드가 호출된다.
이러한 작업을 위해 컴파일러는 AutoCloseable
인터페이스의 구현에 대한 관리 리소스 클래스가 필요하고, 이 인터페이스는 close()
라는 메소드 하나만 가진다.
ARM을 사용한 코드는 간결하지만 프로그래머가 원하는 시간에 클린업을 할려면 추가적인 작업이 필요하다.
클래스가 일정 시간 내에 클린업이 되야하는 heavy한 리소스를 사용한다면 다음과 같이 하면 된다.
우선, 생성자와 close()
를 private
로 선언해 프로그래머가 직접 FileWriteEAM의 인스턴스를 생성할 수 없게 한다. 그래서 factory 메소드가 필요하다. 인스턴스를 생성하고 파라미터로 해당 인스턴스를 전달하는 일번적인 factory 메소드와 달리, 작성할 코드는 인스턴스를 사용자에게 전달하고 작업이 끝날 때까지 기다린다.(람다표현식을 이용하면 가능함)
/*
FileWriterEAM.java
*/
public class FileWriterEAM {
private final FileWriter writer;
private FileWriterEAM(final String fileName) throws IOException {
writer = new FileWriter(fileName);
}
public void writeStuff(final String message) throws IOException {
writer.write(message);
}
private void close() throws Exception {
System.out.println("Close 호출됨");
writer.close();
}
public static void use(final String fileName,
final UseInstance<FileWriterEAM, IOException> block) throws Exception
{
final FileWriterEAM writerEAM = new FileWriterEAM(fileName);
try {
block.accept(writerEAM);
} finally {
writerEAM.close();
}
/* ARM으로 작성해도 된다. (AutoCloseable implements를 받아야한다.)
try (FileWriterEAM writerEAM = new FileWriterEAM(fileName)) {
block.accept(writerEAM);
}
*/
}
public static void main(String[] args) throws Exception {
FileWriterEAM.use("eam.txt", writerEAM -> writerEAM.writeStuff("GOOD!!"));
}
}
/*
UseInstance.java
*/
@FunctionalInterface
public interface UseInstance<T, X extends Throwable> {
void accept(T instance) throws X;
}
use()
메소드에서 두개의 파라미터 fileName, UseInstacne 인터페이스에 대한 레퍼런스를 인수로 받는다. 이 메소드에서 FileWriteEAM을 객체화하고 try와 finally 블록을 생성한다. 그리고 나서 인터페이스가 생성되자마자 accept()
메소드에 FileWriteEAM의 인스턴스를 파라미터로 넘긴다. 호출이 리턴되면, finally 블록에 잇는 인스턴스의 close()
메소드를 호출한다.
@FunctionalInterface
Annotation를 붙여 UseInstance는 함수형 인터페이스로 만들어 자바 컴파일러가 자동으로 람다 표현식이나 메소드 레퍼런스를 합성할 수 있도록 한다.
클레스의 사용자들은 인스턴스를 직접 생성하지 못한다. 이런 특징은 인스턴스가 만료되는 시점 이후로 리소스의 클린업일어나는 코드 생성을 막는 효과를 가져온다.
Using cassandra version : 2.1.8
WARN [GossipTasks:1] 2017-03-21 17:11:05,462 FailureDetector.java:249 - Not marking nodes down due to local pause of 9594745097 > 5000000000
INFO [GossipTasks:1] 2017-03-21 17:11:05,462 Gossiper.java:968 - InetAddress /10.161.26.248 is now DOWN
INFO [Service Thread] 2017-03-21 17:11:05,463 GCInspector.java:252 - ConcurrentMarkSweep GC in 9487ms. CMS Old Gen: 3039553840 -> 3027311712; Par Eden Space: 833093632 -> 0; Par Survivor Space: 46277016 -> 0
이런 로그를 찍고 서버가 down
되었다 살았다를 반복하였다.
Garbage collection pause
라고 한다. 이 현상이 있으면 해당 노드가 다른 노드에 down 된 것처럼 보일 수 있다고 한다.
원인은 아래 4가지 정도가 있다고 한다.
지금 운영 중인 카산드라 특성상 비슷한 TTL이 걸려 있는 데이터들이 많은데, 한달전에 인입된 데이터가 heavy delete
되면서 발생한 문제인것으로 추측한다.
Not marking nodes down due to local pause
로그는 pause 현상이 너무 오래 지속되었을 때 node down이 계속 된 것처럼 보이기 때문에 client에서 심각한 에러가 발생했다고 볼 수 있어서 최대 시간(5초) 보다 크면 node down 표시를 skip 하겠다 라고 알려주는 로그라고한다.
Garbage collection pause
현상은 2.1.6
버전 부터 패치되어 최대시간(5초)동안 서버가 down되었다 살았다를 반복한다.
https://issues.apache.org/jira/browse/CASSANDRA-9183
해결 방안은 크게 두가지 방법이 존재한다.
1번은 현재 사용하고 있는 CMS GC방식이 아닌 Java 1.7부터 도입된 G1 방식으로 튜닝한다.
https://medium.com/@mlowicki/move-cassandra-2-1-to-g1-garbage-collector-b9fb27365509
datastax 에서는 2번 방법 위주로 튜닝 가이드를 제공하고 있다.
MAX_HEAP_SIZE
, HEAP_NEWSIZE
를 조정하는 방법이다.
https://docs.datastax.com/en/cassandra/2.1/cassandra/operations/ops_tune_jvm_c.html
cassandra-env.sh
설정을 보면 이런 계산식에 의해 default 세팅이 되어 있다.
MAX_HEAP_SIZE
= max(min(1/2 ram, 1024MB), min(1/4 ram, 8GB))
HEAP_NEWSIZE
= min(max_sensible_per_modern_cpu_core * num_cores, 1/4 * heap size)
(max_sensible_per_modern_cpu_core 는 100으로 세팅됨)
카산드라 서버들이 모두 24 CPU core, 16G 메모리를 가지고 있으므로
MAX_HEAP_SIZE
는 4G, HEAP_NEWSIZE
는 1G 로 세팅되어 있다.
datastax 가이드 추천대로라면
MAX_HEAP_SIZE
= 8G, HEAP_NEWSIZE
=2G 정도까지 늘릴 수 있을 것 같다.
그리고 현재 서버 메모리를 40%정도만 사용하고 있으므로 큰 영향은 없을 것 같다.
JARFile="KafkaOffsetMonitor-assembly-0.2.1.jar"
DIR="/home1/irteam/seungwan"
function start {
if pkill -0 -f $JARFile > /dev/null 2>&1
then echo "Service [$JARFile] is already running. Ignoring startup request/"
exit 1
fi
echo "Staring [$JARFile] Application!!"
nohup java -cp KafkaOffsetMonitor-assembly-0.2.1.jar com.quantifind.kafka.offsetapp.OffsetGetterWeb --zk tcambkaf-91b901.svr.toastmaker.net,tcambkaf-91b902.svr.toastmaker.net --port 8080 --refresh 10.seconds --retain 2.days> $DIR/log.txt 2>&1 &
}
function stop {
if ! pkill -0 -f $JARFile > /dev/null 2>&1
then
echo "Service [$JARFile] is not running. Ignoring shutdown request."
return 0
fi
echo "Stopping [$JARFile]!!"
# First, we will try to trigger a controlled shutdown using
# Wait until the server process has shut down
attempts=0
while pkill -0 -f $JARFile > /dev/null 2>&1
do
attempts=$[$attempts + 1]
if [ $attempts -gt 5 ]
then
# We have waited too long. Kill it.
pkill -f $JARFile > /dev/null 2>&1
fi
sleep 1s
done
echo "Stop [$JARFile] is completed!!"
}
function status {
if pkill -0 -f $JARFile > /dev/null 2>&1
then echo "Service [$JARFile] is running."
exit 1
fi
echo "Service [$JARFile] is not running"
}
case "$1" in
status)
status
;;
stop)
stop
;;
start)
start
;;
restart)
stop
start
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
esac
exit 0
/dev/null
은 어디에도 콘솔로그를 출력하지 않겠다는 뜻2>&1
은 표준에러는 표준출력에 출력하겠다는 뜻$DIR/log.txt 2>&1
은 표준에러를 파일에 적지 않고 콘솔에 출력하겠다는 뜻 이렇게 2>&1 리다이렉션을 시켜 줌으로 인해 stderr > stdout 으로 출력이 되고 log.txt파일에 기록됨nohup
명령어는 프로세스 중단(hangup)을 무시하고 명령어를 실행한다는 뜻nohup
명령어 끝에 &
를 추가 하면 백그라운드로 실행> /dev/null 2>&1
도 있는데, 이는 표준 출력을 무시하고, 표준에러만 화면에 출력하겠다는 뜻>
연산자는 redirection을 의미함Maven 빌드를 할때, src
이외에 디록토리를 추가가 필요 할때가 있다.
방법은 다음과 같다.
<build>
<resources>
<resource>
<directory>bin</directory>
<targetPath>${basedir}/target/bin</targetPath>
</resource>
<resource>
<directory>conf</directory>
<targetPath>${basedir}/target/conf</targetPath>
</resource>
</resources>
...
</build>
bin
, conf
디렉토리를 빌드 할때 함께 추가한다고 명시하면된다.
src
디렉토리의 java파일을 컴파일해 만들어진 jar파일은 기본 /target아래에 만들어진다.
이 위치를 변경하고 싶으면 다음과 같다.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<outputDirectory>${basedir}/target/lib</outputDirectory>
</configuration>
</plugin>
</plugins>
...
</build>
이 방법은 여러가지가 있다. 그 중 assembly plugin을 사용하면 다음과 같다.
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
dependency에 있는 라이브러리 jar파일을 사용자가 지정한 디렉토리에 저장하는 방법은 다음과 같다.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/lib</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<properties>
<maven.resources.overwrite>true</maven.resources.overwrite>
</properties>
<profiles>
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
</profile>
<profile>
<id>alpha</id>
<properties>
<env>alpha</env>
</properties>
</profile>
<profile>
<id>beta</id>
<properties>
<env>beta</env>
</properties>
</profile>
<profile>
<id>real</id>
<properties>
<env>real</env>
<value>real</value>
</properties>
</profile>
</profiles>
<build>
<finalName>DMP-DATA-API</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/resources-${env}</directory>
</resource>
</resources>
...
</build>
profile에 변수를 등록하고 <resource>
에 directory
path만 적어 주면 된다.
resource diretory가 하나로 합쳐지면서 같은 파일명은 resources-${env}
의 파일 명으로 overwrite 된다.
그 이유는 순차적을 실행되면서 overwrite 되기 때문이다.
(<maven.resources.overwrite>true</maven.resources.overwrite>
이 조건을 추가 해서..)
아래와 같이 resources폴더를 구성하고 mvn clean package -P real
을 하면 순차적으로 copy되는 것을 확인 할 수 있다.
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ dmp-data-api ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 6 resources //resources
[INFO] Copying 3 resources //resources-${env}