이전 포스팅에서 ELK Stack을 어찌저찌 원하는대로 돌아가게끔 구성했습니다.
이번 포스팅에서는 구성 후 받은 피드백과 오류 파티를 해결한 내용을 정리해보고자 합니다.
X-Pack 문제
어느 순간부터 Logstash 를 기동할 때 아래와 같은 오류가 발생하고 데이터 Shipping이 불가능했습니다.
[logstash.licensechecker.licensereader] Attempted to resurrect connection to dead ES instance, but got an error {:url=>”http://elasticsearch:9200/“, :exception=>LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :message=>”Elasticsearch Unreachable: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch: Name or service not known”}
[2022-01-10T06:19:25,065][ERROR][logstash.licensechecker.licensereader] Unable to retrieve license information from license server {:message=>”No Available connections”}
확인 결과, Logstash 설정 중 MONITORING_ELASTICSEARCH_HOSTS 설정이 문제였습니다.
해당 설정은 Elastic Stack X-Pack을 구입한 경우 사용 가능했고, docker-compose.yml에서 Logstash 컨테이너의 MONITORING_ELASTICSEARCH_HOSTS 설정을 제거하고 X-Pack 관련 기능을 아래 설정으로 해제해 해결했습니다.
environment:
- xpack.monitoring.enabled=false
@timestamp 필드와 인덱스의 부재
사실 앞선 포스팅까지 만들고나서 저희 팀장님(그 때 당시에는 팀원이셨던...)께 가져갔습니다.
👦🏻 1HOON | ELK Stack 돌아가게끔 했어요! 어떤가요? |
🧑🏻💻 팀장님 | Elastic Search는 시계열 데이터베이스여서 시계열 데이터가 존재해야해요. 기본적으로 @timestamp 필드를 키로 사용하기 때문에 mutate plugin으로 매핑해주는데, 지금 설정에서는 없네요^^; @timestamp 필드가 없어도 ISO 또는 관습적으로 표현되는 날짜 데이터가 있으면 Kibana에서 자동으로 감지하긴하지만... |
🧑🏻💻 팀장님 | 그리고 mutate와 output에 인덱스명이 없습니다. 시계열 데이터베이스이고 시계열은 기본적으로 대용량을 기반으로 하기 때문에 파티셔닝은 필수!! 파티셔닝은 인덱스명으로 이루어지기 때문에 보통 indexname-yyyy-mm-dd와 같이 인덱스를 가져갑니다. 파티셔닝이 되면 데이터 조회, 어그리게이션을 할 때 필요한 인덱스만 바라보기 때문에 빠르죠!! |
👦🏻 1HOON | 🤦🏻♂️ (고칠게 산더미네...) |
피드백을 받고나서 파이프라인을 수정했습니다.
filter {
json {
source => "message"
target => "info"
}
date {
match => ["[info][collectedDateTime]", "ISO8601"]
target => "@timestamp"
}
...
}
output {
elasticsearch {
hosts => ["http://es01:9200","http://es02:9200","http://es03:9200"]
index => "data-collector-%{+YYYY.MM.dd}"
}
}
json
읽은 데이터의 message 필드를 JSON 형태로 변환하여 info 필드로 저장했습니다.
date
info 필드의 collectedDateTime 필드가 ISO8601 형식일 경우 @timestamp 필드에 해당 필드 값을 저장하도록 했습니다.
output
elasticsearch로 데이터를 전송할 때, index를 `data-collector-연도월일` 형태로 지정했습니다.
위험한 패턴
앞서 ELK Stack을 연동하면서 Data Collector의 FileWriter 로직을 수정했었습니다.
이 부분에 대해서도 피드백을 받았는데, 임시 파일의 이동과 생성이 Atomic하지 않기 때문에 상당히 위험한 패턴이라고 피드백주셨습니다.
현재 방식으로도 동시성을 고려해 안전하게 만들 수 있지만, 패턴의 위험성, 성능 저하, 코드 복잡도 상승이 뒤따르기 때문에 안전한 패턴으로의 변경을 권유해주셨습니다.
변경한 로직은 아래와 같습니다.
- Data Collector는 1시간 단위로 로컬 저장소 디렉터리에 새 파일을 생성합니다.
- Data Collector는 1초 단위로 로컬 저장소 디렉터리의 파일에 데이터를 씁니다.
- Logstash는 로컬 저장소 디렉터리의 파일을 읽어 Elastic Search로 전송합니다.
- 3번 작업 완료 후 읽은 파일은 삭제하지 않고, 읽은 파일과 위치를 기억합니다.
변경한 로직을 적용하기 위해 Data Collector의 작업 내용을 롤백하고, 파이프라인 설정을 변경했습니다.
input {
file {
path => "/source-data/*.txt"
delimiter => "
"
file_sort_by => "path"
mode => "tail"
start_position => "beginning"
file_completed_action => "log"
file_completed_log_path => "/logs/file_completed_log.log"
}
}
start_position
- 파일의 맨 앞부터 읽도록 beginning으로 지정했습니다.
- 단, 이 설정은 파일을 최초로 읽는 시점에만 적용됩니다.
file_completed_action
- log 로 변경해 파일이 제거되는 것을 막았습니다.
저는 꼭 삭제해줘야만 하는줄 알았어요...
사실 이런 위험한 패턴을 사용한 이유는 제가 Logstash를 제대로 학습하지 않아서였습니다.
읽은 파일은 삭제해줘야 데이터를 중복으로 읽지 않을 거라고 생각했거든요.
사실 Logstash는 여러번 재기동되더라도 기존에 읽은 파일을 처음부터 읽지 않습니다. 왜 그럴까요?
바로 Logstash에서 since_db라는 파일에 읽은 파일의 트래킹 정보를 기록하기 때문입니다.
파일의 트래킹 정보로 Logstash가 재시작되었을 때 마지막 읽은 위치부터 데이터를 읽게 되는 것입니다.
때문에 반드시 봐야할 중요한 속성은 sincedb_clean_after와 sincedb_path, start_position입니다.
- sincedb_clean_after : sincedb의 파일 트래킹 정보의 저장 기간을 지정합니다. 기본값은 `2 weeks`입니다.
- sincedb_path : sincedb 파일의 위치를 지정합니다. 폴더가 아닌 파일 형태의 경로이어야합니다.
- start_position : 파일을 처음에 읽을 때 읽기 시작할 위치입니다. 기본값은 `end`입니다.
delimiter 인식 안됨
Logstash 파이프라인에서 delimiter를 `\n`로 지정했는데, 정상적으로 동작하지 않았습니다.
Elastic Discuss에서 관련 토픽을 찾았습니다.
이 토픽의 작성자는 저처럼 개행으로 데이터를 구분하는 것을 의도해 delimiter를 `\n`으로 지정했으나 동작하지 않았다고 합니다.
그런데 delimiter 설정을 제거하니 정상적으로 동작했다고 하네요.
이 토픽의 채택된 답변은 아래와 같았습니다.
정리하자면, 이미 정상적으로 개행이 된 파일을 읽을 때는 delimiter를 지정할 필요가 없다고합니다.
Logstash가 구동되는 환경이 Window일 경우 Window에 맞게, UNIX일 경우 UNIX에 맞게 개행을 감지한다고하네요.
그래도 설정을 하고자한다면, 예시 코드와 같이 그냥 개행해주면 된다고합니다.
위 답변대로 파이프라인을 수정했고, 정상적으로 해결되었습니다.
input {
file {
...
delimiter => "
"
...
}
}
JSON flatten 문제
Data Collector로 요청한 수집 데이터(JSON)가 message라는 하나의 필드에 String 형태로 저장되었습니다.
하나의 필드에 수집 데이터가 저장되면 데이터의 특정 필드를 검색하는데 문제가 발생할 것이기 때문에 flatten 작업을 추가해주었습니다.
filter {
json {
source => "message"
target => "info"
}
...
json {
source => "[info][data]"
target => "data"
}
...
}
파이프라인 설정을 변경하고 다시 수집 요청을 하면 아래와 같이 수집 데이터가 flatten 되어 저장됩니다.
Client IP 와 위치 정보 데이터 추가
팀장님 조언으로 클라이언트의 IP와 위치 정보 데이터를 추가했습니다.
Client IP
Data Collector의 Controller에서 클라이언트 IP를 얻어와 데이터에 포함시켰습니다.
private static final String[] REMOTE_IP_HEADERS = { "X-Forwarded-For" , "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP", "HTTP_FORWARDED_FOR", "HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR"};
/**
* 클라이언트의 IP를 반환합니다.
*
* @return {@link #REMOTE_IP_HEADERS} 헤더가 존재할 경우 첫번째 IP
*/
public static String getRemoteIp() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return "0.0.0.0";
}
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
for (String header : REMOTE_IP_HEADERS) {
String requestHeader = request.getHeader(header);
if (StringUtils.hasText(requestHeader)) {
return requestHeader.split(",")[0];
}
}
return request.getRemoteAddr();
}
REMOTE_IP_HEADERS에 있는 헤더를 검사해 아이피 정보가 있는 경우 클라이언트의 아이피로 간주하게끔 했습니다.
REMOTE_IP_HEADERS에 포함된 헤더들은 비표준 헤더들인데, LoadBalancer나 Proxy를 통해 애플리케이션으로 요청이 전달될 경우 이 헤더들 중에 하나에 클라이언트의 아이피와 LB/Proxy의 아이피를 담아주고 있습니다.
위치 정보 데이터
Logstash의 geoip filter plugin을 사용해 앞서 얻은 클라이언트의 IP를 기반으로 위치정보를 만들어 Kibana에서 시각화 데이터로 사용하게끔 했습니다.
...
filter {
json {
source => "message"
target => "info"
}
date {
match => ["[info][collectedDateTime]", "ISO8601"]
target => "@timestamp"
}
json {
source => "[info][data]"
target => "data"
}
geoip {
source => "[info][clientIp]"
}
}
...
geopip
- JSON으로 파싱된 필드의 info.clientIp 필드를 사용해 위치 정보를 생성해 데이터에 추가합니다.
- GeoLite2 데이터베이스가 번들로 제공됩니다.
결과
모든 수정을 마치고, 수집된 데이터를 Kibana에서 아래와 같이 확인할 수 있습니다.
'📦 ETC > TOY PROJECT' 카테고리의 다른 글
[찍어먹기] Spring Boot 부터 ELK Stack 까지 :: 트러블슈팅 (1) - Elasticsearch (0) | 2022.03.17 |
---|---|
[찍어먹기] Spring Boot 부터 ELK Stack 까지 :: 인증인가 처리 (0) | 2022.03.07 |
[찍어먹기] Spring Boot 부터 ELK Stack 까지 :: 데이터 수집해서 시각화 하기 (1) (0) | 2022.01.30 |
[찍어먹기] Spring Boot 부터 ELK Stack 까지 :: 반 정형 데이터 수집기 만들기 (0) | 2022.01.02 |