다음 명령을 통해 next.js 프로젝트를 생성한다.

sevity@sevityubuntu:~/workspace/online_judge$ mkdir problem-frontend
sevity@sevityubuntu:~/workspace/online_judge/problem-frontend$ npx create-next-app .
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias? … No / Yes

Would you like to use TypeScript? - 추천: 예
Would you like to use ESLint? - 추천: 예
Would you like to use Tailwind CSS? - 추천: 아니오
Would you like to use src/ directory? - 추천: 예
Would you like to use App Router? - 추천: 예
Would you like to customize the default import alias? - 추천: 아니오

 

Bootstrap 테마변경

 Bootswatch와 같은 웹사이트에서 무료로 제공하는 Bootstrap 테마를 선택가능

테마적용방법


1. 테마 CSS 파일 다운로드: 먼저, 테마 페이지에 접속. 페이지 상단의 "Download" 버튼을 클릭하고, "bootstrap.min.css" 파일을 다운로드

2. 프로젝트에 CSS 파일 추가: 다운로드한 CSS 파일을 public/css 디렉토리를 만들고 그 안에 CSS 파일을 추가

3. CSS 파일 import: _app.js 파일을 열고, 다음 코드를 추가

import '../public/css/bootstrap.min.css';

 

next.js pages

/create 아래와 같이 문제를 새로 입력한다.(아래는 아직 실제입력,실제출력등 빠진부분들이 많다)

 

/problems 문제 목록을 아래와 같이 출력한다.

(로그인 상태가 아니라서 username이 없을 경우, frontend-service/login 으로 direct 된다)

 

 

 

 

 

 

 

반응형

문제관리 백엔드 서비스는 online-judge프로젝트의 7개 서비스중 3번째 서비스이다.

  1. 인증 서비스 (Backend): 사용자의 회원 가입, 로그인, 로그아웃, 세션 관리 등을 담당
  2. 인증 서비스 (Frontend): 사용자 인터페이스를 제공 (로그인 폼, 회원가입 폼 등)
  3. 문제 관리 서비스 (Backend): 문제의 추가, 삭제, 수정 등을 관리
  4. 문제 관리 서비스 (Frontend): 문제를 보여주고, 문제 추가, 삭제, 수정 등의 인터페이스를 제공
  5. 제출 관리 서비스 (Backend): 사용자의 코드 제출 및 제출 기록을 관리
  6. 제출 관리 서비스 (Frontend): 코드 제출 인터페이스와 제출 기록 확인 인터페이스를 제공
  7. 채점 서비스 (Backend): 제출된 코드를 채점

다른 서비스는 링크를 눌러확인하자.

 

문제관리 백엔드 서비스는 java/vscod가 아닌 kotlin/IntelliJ로 해보기로 했다. (kotlin호환성이 IntelliJ가 훨씬 좋음)

디렉토리 구성은 다음과 같다.

sevity@sevityubuntu:~/workspace/online_judge/problem-service$ tree -I target
.
├── HELP.md
├── log
│   └── application.log
├── mvnw
├── mvnw.cmd
├── pom.xml
├── run.sh
└── src
    ├── main
    │   ├── kotlin
    │   │   └── com
    │   │       └── sevity
    │   │           └── problemservice
    │   │               ├── controller
    │   │               │   └── ProblemController.kt
    │   │               ├── domain
    │   │               │   ├── Problem.kt
    │   │               │   └── ProblemRepository.kt
    │   │               ├── ProblemServiceApplication.kt
    │   │               └── service
    │   │                   └── ProblemService.kt
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── kotlin
            └── com
                └── sevity
                    └── problemservice
                        └── ProblemServiceApplicationTests.kt

18 directories, 13 files

 

문제에 대한 스키마를 다음과 같이 설정했다.

제대로 하려면, real_input, real_output, solution.cpp 등이 추가되어야 하지만, 우선은 간단하게 했다.

CREATE TABLE problems (
  id SERIAL PRIMARY KEY,
  title VARCHAR(100) NOT NULL,
  description TEXT NOT NULL,
  example_input TEXT NOT NULL,
  example_output TEXT NOT NULL
);

 

 

초기 IntelliJ설정

Ultimate버전과 Community버전이 있는데 전자만 SpringBoot관련 기능이 제공된다.

먼저 로컬환경에서 IntelliJ를 실행한 후,  File > New > Project > Spring Initializr를 통해 프로젝트를 로컬에 생성한다.

위의 내용은 프로그램관리 백엔드에 해당하진 않는다. 개요만 참조하자.

그다음 생성된 파일들을 ssh환경으로 원격복사한다.

 

그다음음 다음과 같이 Remote Development > SSH로 들어가서 원격 개발환경을 설정한다.

application.properties 파일을 다음과 같이 설정. 포트번호는 7개 서비스중 3번째라는 의미로 8083으로 부여.

spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DATABASE_USERNAME}
spring.datasource.password=${DATABASE_PASSWORD}
spring.jpa.hibernate.ddl-auto=update
logging.file.name=log/application.log
# without below line, web login is needed.
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
server.port=8083

알고리즘 문제(problem)에 대한 도메인 entity와 repository를 domain이라는 패키지(폴더)에 다음과 같이 작성한다. 위의 DB스키마와 알맞도록 작성.

먼저 Problem entity는 다음과 같이 작성

package com.sevity.problemservice.domain

import javax.persistence.*

@Entity
@Table(name = "problems")
data class Problem(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(nullable = false)
    val title: String = "",

    @Column(nullable = false)
    val description: String = "",

    @Column(name = "example_input", nullable = false)
    val exampleInput: String = "",

    @Column(name = "example_output", nullable = false)
    val exampleOutput: String = ""
)

JPA로 DB와 연동을 자동화해주는 Repository는 다음과 같이 작성

package com.sevity.problemservice.domain

import com.sevity.problemservice.domain.Problem
import org.springframework.data.jpa.repository.JpaRepository

interface ProblemRepository : JpaRepository<Problem, Long>

 

MVC에서 C에 해당하는 Control 클래스를 아래와 같이 작성

package com.sevity.problemservice.controller

import com.sevity.problemservice.domain.Problem
import com.sevity.problemservice.service.ProblemService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/problems")
class ProblemController(private val problemService: ProblemService) {

    @GetMapping
    fun getAllProblems() = problemService.getAllProblems()

    @GetMapping("/{id}")
    fun getProblem(@PathVariable id: Long): Problem = problemService.getProblem(id)


    @PostMapping
    fun createProblem(@RequestBody problem: Problem): Problem = problemService.createProblem(problem)

    @PutMapping("/{id}")
    fun updateProblem(@PathVariable id: Long, @RequestBody problem: Problem): Problem = problemService.updateProblem(id, problem)

    @DeleteMapping("/{id}")
    fun deleteProblem(@PathVariable id: Long): ResponseEntity<Void> {
        problemService.deleteProblem(id)
        return ResponseEntity<Void>(HttpStatus.NO_CONTENT)
    }

    // Add more methods as needed for CRUD operations
}

여기서 /problems라는 하나의 자원(동사가 아닌명사)에 대해서 GET, POST, PUT, DELETE를 모두 사용하는 RESTFul 권장 패턴을 사용하고 있다. problems로 복수형으로 지칭하는 것도 권장 가이드.

 

실제로 호출해 보는 것은 curl을 써도 되지만, postman을 쓰면 편하게 할 수 있다.

postman은 여기서 다운받고,

실행후 new > HTTP를 통해서 GET, POST, PUT, DELETE를 하나씩 테스트 하면된다.

GET은 문제 전체나 특정 id를 받아올 수 있고 아래처럼 url만 지정하고 SEND하면 된다.(결과에서 200 OK확인)

POST는 문제를 등록하는 과정이고, 

headers 탭에 Key: Content-Type, Value: application/json 을 추가하고

body에서 raw를 선택하고 아래처럼 문제내용을 입력하고 SEND해주면 된다.(결과에서 200OK확인)

PUT은 문제를 업데이트 하는 과정이고 POST와 마찬가지 설정으로 하면 된다.(POST, PUT만 다르고 나머진 동일)

DELETE는 지우고자 하는 문제ID를 url끝에 넣어주기만 하면 되고, 200OK가 아닌 204 No Content가 나오면 성공.

 

MVC에서 M과 C를 연결하는 비즈니스 로직에 해당하는 Service클래스를 아래와 같이 작성

package com.sevity.problemservice.service

import com.sevity.problemservice.domain.Problem
import com.sevity.problemservice.domain.ProblemRepository
import org.springframework.stereotype.Service

@Service
class ProblemService(private val problemRepository: ProblemRepository) {

    fun getAllProblems(): List<Problem> = problemRepository.findAll()
    fun createProblem(problem: Problem): Problem = problemRepository.save(problem)
    fun getProblem(id: Long): Problem = problemRepository.findById(id).orElseThrow { NoSuchElementException("Problem not found") }
    fun updateProblem(id: Long, problem: Problem): Problem {
        val existingProblem = problemRepository.findById(id).orElseThrow { NoSuchElementException("Problem not found") }
        val updatedProblem = existingProblem.copy(
            title = problem.title,
            description = problem.description,
            exampleInput = problem.exampleInput,
            exampleOutput = problem.exampleOutput
        )
        return problemRepository.save(updatedProblem)
    }
    fun deleteProblem(id: Long) {
        val existingProblem = problemRepository.findById(id).orElseThrow { NoSuchElementException("Problem not found") }
        problemRepository.delete(existingProblem)
    }

}

 

트러블슈팅


원격으로 열었더니 ._로 시작하는 중복파일들이 생길 경우

._로 시작하는 파일들은 macOS에서 생성하는 메타데이터 파일들이다. 이 파일들은 macOS 외의 시스템에서는 필요하지 않으며 삭제해도 안전하다. 다음 명령으로 삭제

find . -name '._*' -type f -delete

1회 지운 이후로 문제가 생긴적은 없다.

 

javax.persistance 관련 오류

springboot 버전을 3이상으로 했더니, import javax.persistance 에 실패하는 현상 발견됨

여기 참고해서, pom.xml에서 아래처럼 2.5.3으로 버전을 낮춰서 해결

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

 

 

 

반응형

kafka란

RabbitMQ와 같은 메시지 미들웨어.

LinkedIn에서 개발되어 현재는 Apache Software Foundation의 일부인 오픈 소스 메시지 스트리밍 플랫폼.
대량의 실시간 데이터 스트리밍을 처리하는 데 초점을 맞추고 있고, 높은 처리량, 데이터 복제, 분산 처리 등을 지원

이를 통해 대규모 데이터를 실시간으로 처리할 수 있다.

MSA간 통신에도 자주 쓰임

 

브로커: Kafka에서 메시지를 처리하는 서버

  • 카프카에서는 이벤트를 구분하기 위한 단위로 '토픽'을 사용하며 파일시스템의 디렉토리와 비슷한 개념으로 이해 가능
    • 토픽은 게시/구독 메시징 시스템에서 흔하게 볼 수 있다.
      • 카프카 외적으로는 비슷한 개념에 대해 채널, 큐, 스트림이라는 용어를 사용하기도 함
        • 개인적으로는 채널이 더 직관적이네
    • 프로듀서는 데이터를 '토픽'에 넣고, 컨슈머는 '토픽'에서 데이터를 읽는다.
    • 이러한 토픽은 카프카 클러스터내에서 데이터를 분류하고 구독자가 관심있는 메시지만 구독할 수 있도록 해주는 중요한 역할
    • 카프카 클러스터 전체가 하나의 토픽만 사용한다면 생략도 가능한가? > NO.
  • 토픽의 데이터는 파티션에 분산 저장되며 각 파티션은 순서 유지가 되는 메시지의 로그로 구성
  • 하나의 브로커는 여러 파티션의 데이터를 저장할 수 있으며, 반대로 하나의 파티션도 여러 브로커에 복제될 수 있음에 주의
  • 파티션의 개수는 토픽의 병렬처리 능력을 결정

리더 브로커: 특정 메시지그룹(파티션)의 처리를 담당하는 서버. Kafka에서 각 데이터 파티션에는 하나의 리더 브로커가 있음

  • 리더 브로커는 파티션과 1:1의 관계를 가지며 하나의 파티션을 여러 브로커가 처리할경우 하나만 리더이고 나머지는 팔로워로 동작

 

ZooKeeper

  • 일종의 비서역할로 서버들의 상태관리나 역할분배를 Kafka를 위해서 수행함. Kakfa설치시 같이 설치됨.
  • 또는 일종의 '중앙 데이터 저장소' 역할. Kafka 시스템 내의 여러 서버들이 ZooKeeper를 통해 필요한 정보 공유하고 동기화.
  • 특정 서버에 문제가 생기면 이 정보가 ZooKeeper에 기록되고 다른 서버들이 이를 확인하여 적절히 대응

 

kafka설치과정

Java설치

카프카는 Java기반이므로 java jdk가 안깔렸으면 깔아준다.

sudo apt update
sudo apt install default-jdk

사용자계정에 종속된 설치보다는 여러명이 쓸 수 있도록 아래 처럼 전용 유저를 생성해주는게 좋다.

sudo adduser kafka
sudo adduser kafka sudo

카프카는 자바 기반이므로 jar파일을 받아서 설정해준다. apt등의 패키지 매니저를 사용하면 예전버전일수 있으므로 다운로드해서 해보자.

mkdir ~/Downloads
curl "https://downloads.apache.org/kafka/3.5.1/kafka_2.13-3.5.1.tgz" -o ~/Downloads/kafka.tgz
mkdir /opt/kafka && cd /opt/kafka
tar -xvzf ~/Downloads/kafka.tgz --strip 1


#kafka디렉토리 소유권을 kafka사용자로 변경
sudo useradd kafka -m
sudo chown -R kafka:kafka /opt/kafka

/opt/kafka에 설치했으며, /opt 디렉토리는 선택적인(add-on) 애플리케이션 SW패키지를 위한 곳으로 여러사용자가 이용하는 라이브러리인 경우 선호되는 경로다.

 

zookeeper설정

zookeeper는 kafka와 독립적으로 실행되며, 비슷한 설정 매커니즘을 갖는다.

zookeeper설정파일 수정

sevity@sevityubuntu:/opt/kafka/config$ cat zookeeper.properties
# the directory where the snapshot is stored.
dataDir=/var/cache/zookeeper
# the port at which the clients will connect
clientPort=2181
# disable the per-ip limit on the number of connections since this is a non-production config
maxClientCnxns=0
# Disable the adminserver by default to avoid port conflicts.
# Set the port to something non-conflicting if choosing to enable this
admin.enableServer=false
# admin.serverPort=8080

다른 부분은 그냥 두었고, dataDir은 /var/cache 밑에 두어 휘발되지 않도록 설정했다. (기본은 /tmp 밑에 있었던듯)

이를 위해 다음과 같은 권한 설정을 했다.

sudo mkdir /var/cache/zookeeper
sudo chown kafka:kafka /var/cache/zookeeper
sudo chmod 700 /var/cache/zookeeper

 

zookeeper에 대해 systemctl에 등록해서 부팅시 마다 실행되도록 하기위해 다음 파일 작성

sudo vi /etc/systemd/system/zookeeper.service

[Unit]
Description=Apache Zookeeper server
Documentation=http://zookeeper.apache.org
Requires=network.target remote-fs.target
After=network.target remote-fs.target

[Service]
Type=simple
User=kafka
ExecStart=/opt/kafka/bin/zookeeper-server-start.sh /opt/kafka/config/zookeeper.properties
ExecStop=/opt/kafka/bin/zookeeper-server-stop.sh
Restart=on-abnormal

[Install]
WantedBy=multi-user.target

# 그 다음 systemctl을 통해 시작하고 재부팅 시에도 항상 실행되도록 설정
sudo systemctl start zookeeper
sudo systemctl enable zookeeper

kafka에 대해서도 마찬가지로 작성

sudo vi /etc/systemd/system/kafka.service

[Unit]
Description=Apache Kafka server
Documentation=http://kafka.apache.org
Requires=zookeeper.service
After=zookeeper.service

[Service]
Type=simple
User=kafka
ExecStart=/home/kafka/kafka/bin/kafka-server-start.sh /home/kafka/kafka/config/server.properties
ExecStop=/home/kafka/kafka/bin/kafka-server-stop.sh
Restart=on-abnormal

[Install]
WantedBy=multi-user.target

# 그 다음 systemctl을 통해 시작하고 재부팅 시에도 항상 실행되도록 설정
sudo systemctl start kafka
sudo systemctl enable kafka

 

트러블 슈팅

서버로그 실시간 모니터링

tail -f /opt/kafka/logs/server.log

 

jps로 kafka가 잘 실행되고 있는지 확인(QuorumPeerMain과 Kafka가 떠있으면OK)

su - kafka
jps
109092 Jps
108186 QuorumPeerMain(Zookeeper의 시작점)
108573 Kafka

 

 

토픽 메시지가 잘 수신되는지 확인하는 방법

가장 기본적으로는 9092포트를 리슨하고 있는지 확인

sevity@sevityubuntu:/opt/kafka/config$ lsof -i :9092
COMMAND    PID   USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
java    168958 sevity  116u  IPv6 1324110      0t0  TCP sevityubuntu:46330->sevityubuntu:9092 (ESTABLISHED)
java    168958 sevity  117u  IPv6 1320714      0t0  TCP sevityubuntu:46336->sevityubuntu:9092 (ESTABLISHED)

 

GetOffsetSell을 통해 특정 토픽에 게시된 메지시수를 확인

sevity@sevityubuntu:/opt/kafka$ bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic iis_log --time -1
iis_log:0:4043038

kafka-console-consumer.sh를 사용하여 특정 토픽의 메시지 수신상황을 실시간으로 확인

$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic iis_log --from-beginning
{"@timestamp":"2023-08-01T01:18:34.407Z","@metadata":{"beat":"filebeat","type":"_doc","version":"8.9.0"},"agent":{"version":"8.9.0","ephemeral_id":"98df9137-96e3-4653-b353-2821eebde875","id":"cf839823-29c8-4619-b9c4-85e90804e50e","name":"SEVITY-PC","type":"filebeat"},"ecs":{"version":"8.0.0"},"log":{"file":{"path":"c:\\inetpub\\logs\\LogFiles\\W3SVC1\\u_ex230801.log"},"offset":16076},"message":"2023-08-01 01:17:24 192.168.0.6 GET /wiki/doku.php id=Advice%20To%20Relieve%20Your%20Acid%20Reflux%20Signs%20and%20symptoms 80 - 196.245.181.117 Mozilla/5.0+(X11;+Ubuntu;+Linux+x86_64;+rv:114.0)+Gecko/20100101+Firefox/114.0 http://sevity.com/ 200 0 0 1182","input":{"type":"log"},"host":{"mac":["00-15-83-EA-2C-EE","00-23-24-63-E7-E3","02-50-41-00-00-01","88-36-6C-F7-D9-04","88-36-6C-F7-D9-06","88-36-6C-F7-D9-07"],"hostname":"sevity-pc","name":"sevity-pc","architecture":"x86_64","os":{"family":"windows","name":"Windows 10 Enterprise","kernel":"10.0.19041.3208 (WinBuild.160101.0800)","build":"19045.3208","type":"windows","platform":"windows","version":"10.0"},"id":"dbb18b3d-fb42-49ec-b731-f430dd2f3fd5","ip":["fe80::a30b:d99a:f7ed:2bac","192.168.100.15","fe80::a4ce:c09a:ea91:8a9","169.254.142.88","fe80::9c93:77c8:66bd:b6c5","169.254.55.217","fe80::dd77:ec13:69c4:6922","169.254.56.104","fe80::310a:1b3d:e9a3:431c","192.168.0.6","fe80::2c27:d03a:a322:765b","169.254.236.36"]}}

 

반응형

'Programming > Linux' 카테고리의 다른 글

Nginx  (0) 2023.08.16
Redis  (0) 2023.08.10
ELK연습  (0) 2023.07.30
스팍 - 실습 - 웹서버 세션분석  (0) 2023.07.30
스팍(Spark) 설치  (0) 2023.07.30

먼저 IIS서버에 Filebeat를 깔아서 우분투 분석 서버에 kafka전송을 한다.

반응형

'Programming > Linux' 카테고리의 다른 글

Redis  (0) 2023.08.10
kafka 설치(우분투 기존)  (0) 2023.07.31
스팍 - 실습 - 웹서버 세션분석  (0) 2023.07.30
스팍(Spark) 설치  (0) 2023.07.30
하둡 - 실습 - 웹서버 세션분석  (0) 2023.07.29

먼저 하둡버전을 보고 오길 권한다.

스팍 설치에 관한 부분은 여기 참조.

우리는 하둡버전과 비슷하게 IIS웹서버를 분석해서 map - reduce과정을 통해 IP별 접근 시간및 URL을 정리해서 볼 것이다. 오리지널 웹서버 로그는 대략 다음과 같으며,

sevity@sevityubuntu:~/workspace/hadoop_sevity.com/session_analysis$ cat ../iis_logs/* | head
#Software: Microsoft Internet Information Services 10.0
#Version: 1.0
#Date: 2015-09-06 08:32:43
#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
2015-09-06 08:32:43 127.0.0.1 GET / - 80 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/45.0.2454.85+Safari/537.36 - 200 0 0 211
2015-09-06 08:32:43 127.0.0.1 GET /iisstart.png - 80 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/45.0.2454.85+Safari/537.36 http://127.0.0.1/ 200 0 0 2
2015-09-06 08:32:43 127.0.0.1 GET /favicon.ico - 80 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/45.0.2454.85+Safari/537.36 http://127.0.0.1/ 404 0 2 2

map-reduce를 거친 결과는 대략 다음과 같을 것이다.

sevity@sevityubuntu:~/workspace/hadoop_spark/session_analysis$ cat session_output_merged.txt | head
157.55.39.90
[[(datetime.datetime(2016, 6, 7, 20, 44, 21), '/')], [(datetime.datetime(2016, 6, 8, 4, 20, 31), '/menu_wonilnet.asp'), (datetime.datetime(2016, 6, 8, 4, 20, 43), '/menu_friend.asp')], [(datetime.datetime(2019, 9, 16, 23, 58, 6), '/menu_friend.asp')], [(datetime.datetime(2021, 8, 17, 15, 8, 50), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 17, 17, 36, 23), '/menu_wonilnet.asp'), (datetime.datetime(2021, 8, 17, 17, 36, 24), '/menu_friend.asp')], [(datetime.datetime(2021, 8, 18, 1, 53, 52), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 19, 2, 40, 48), '/wiki/doku.php')], [(datetime.datetime(2021, 8, 19, 6, 45, 32), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 20, 10, 0, 31), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 21, 3, 55, 36), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 21, 4, 55, 13), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 23, 6, 49, 26), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 24, 1, 45, 47), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 24, 9, 58, 34), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 25, 11, 55, 24), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 26, 8, 22, 7), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 26, 13, 56, 19), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 27, 21, 59, 24), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 28, 20, 3, 46), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 28, 23, 51, 21), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 29, 18, 40, 40), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 29, 20, 6, 8), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 31, 1, 38, 52), '/wiki/doku.php')], [(datetime.datetime(2021, 8, 31, 5, 29, 5), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 8, 31, 6, 28, 20), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 9, 2, 0, 32, 17), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 9, 2, 16, 19, 20), '/wiki/lib/exe/detail.php')], [(datetime.datetime(2021, 9, 9, 3, 2, 43), '/menu_wonilnet.asp'), (datetime.datetime(2021, 9, 9, 3, 2, 45), '/menu_friend.asp')], [(datetime.datetime(2021, 9, 16, 7, 52, 4), '/wiki/doku.php')]]
138.197.96.197
[[(datetime.datetime(2017, 2, 15, 17, 21, 24), '/'), (datetime.datetime(2017, 2, 15, 17, 21, 26), '/menu_wonilnet.asp'), (datetime.datetime(2017, 2, 15, 17, 21, 26), '/board/dhtml_logo.js')]]
104.236.164.185
[[(datetime.datetime(2017, 11, 28, 11, 45, 56), '/')]]
181.213.93.231
[[(datetime.datetime(2018, 1, 30, 13, 1, 24), '/hndUnblock.cgi'), (datetime.datetime(2018, 1, 30, 13, 1, 28), '/tmUnblock.cgi'), (datetime.datetime(2018, 1, 30, 13, 1, 32), '/moo'), (datetime.datetime(2018, 1, 30, 13, 1, 36), '/'), (datetime.datetime(2018, 1, 30, 13, 1, 42), '/getcfg.php'), (datetime.datetime(2018, 1, 30, 13, 1, 49), '/getcfg.php')]]
117.80.147.63
[[(datetime.datetime(2018, 2, 14, 8, 13, 12), '/')]]

실습을 위한 디렉토리 구조는 대략 다음과 같다.

sevity@sevityubuntu:~/workspace/hadoop_spark$ tree -L 3
.
├── iis_logs (여기에 분석하려는 웹로그 파일들이 있습니다. 실제로는 수없이 많음)
│   ├── u_ex230101.log
│   ├── u_ex230102.log
│   └── u_ex230103.log
├── session_analysis
│   ├── session_output
│   │   ├── part-00001
│   │   ├── part-00002
│   │   ├── part-00003
│   │   └── _SUCCESS
│   ├── session_output_merged.txt
│   └── spark_session_analysis.py(우리가 작성할 python 코드의 위치)
└── spark_test

다음과 같이 spark_session_analysis.py를 작성하자.

여기서 중요한 점은 다음과 같다.

  • 스팍은 하둡과 다르게 하나의 파일에서 map과 reduce를 모두 수행한다.
  • 각 동작은 함수호출 형태로 진행되며,
  • 아래에서 reduce에 해당하는 함수는 groupByKey()이다.
from pyspark import SparkConf, SparkContext
from datetime import datetime, timedelta
from pyspark.sql import SparkSession

def parse_log_line(line):
    try:
        parts = line.strip().split()
        if len(parts) != 15:
            return None

        date_time = parts[0] + " " + parts[1]
        date_time = datetime.strptime(date_time, '%Y-%m-%d %H:%M:%S')
        url = parts[4]
        ip = parts[8]  # changed from parts[2] to parts[8] to use the client IP

        return (ip, (date_time, url))
    except:
        return None

def calculate_sessions(group):
    ip, data = group
    data = list(data)
    data.sort(key = lambda x: x[0])
    sessions = []
    session = [data[0]]
    for i in range(1, len(data)):
        if data[i][0] - session[-1][0] > timedelta(seconds=1800):
            sessions.append(session)
            session = [data[i]]
        else:
            session.append(data[i])
    sessions.append(session)
    return (ip, sessions)


# Spark configuration
# [*]하면 모든 코어 사용
conf = SparkConf().setMaster('local[*]').setAppName('Log Analysis')
sc = SparkContext(conf=conf)

log_lines = sc.textFile('hdfs://localhost:9000/logs/u_ex*')
parsed_lines = log_lines.map(parse_log_line).filter(lambda x: x is not None)
grouped_by_ip = parsed_lines.groupByKey()
sessions = grouped_by_ip.flatMap(calculate_sessions)
sessions.saveAsTextFile('hdfs://localhost:9000/session_output')

다음 명령을 통해 실행하고 결과를 확인한다.

# hdfs에 reduce결과를 쌓을 곳을 clean-up해준다. 
hdfs dfs -rm -r /session_output

# 로컬파일시스템에 복사해올 경로도 clean-up해준다.
rm -rf session_output
rm -f session_output_merged.txt

# python 파일을 실행한다. spark-submit을 통해 실행하면 4040포트를 통해 웹에서 진행상황 확인가능
$SPARK_HOME/bin/spark-submit spark_session_analysis.py 2>&1 | tee r.txt 

# HDFS에서 로컬파일시스템으로 결과 폴더 복사
hadoop fs -get /session_output .

# 위의 폴더구조에서 볼 수 있듯 병렬처리 때문에 결과파일이 여러개로 분리돼 있는데 합쳐준다.
# 합치고 로컬파일시스템으로 복사하는 것까지.
hadoop fs -getmerge hdfs://localhost:9000/session_output ./session_output_merged.txt

# 위의 방법대신 python코드내에서 다음 방법으로 머지해도 된다.
# 단 스팍을 써서 머지하는 방법인 만큼 메모리가 터질수도 있으니 주의
sessions.coalesce(1).saveAsTextFile('hdfs://localhost:9000/session_output')
반응형

'Programming > Linux' 카테고리의 다른 글

kafka 설치(우분투 기존)  (0) 2023.07.31
ELK연습  (0) 2023.07.30
스팍(Spark) 설치  (0) 2023.07.30
하둡 - 실습 - 웹서버 세션분석  (0) 2023.07.29
하둡 - 실습 - 웹서버로그분석  (0) 2023.07.29

기본개념

스파크(Spark)는 하둡과 마찬가지로 자바(Java) 기반의 빅 데이터 처리 프레임워크입니다. 
하지만 스파크는 자바 외에도 스칼라(Scala), 파이썬(Python), R 등 여러 언어를 지원합니다.

스파크의 핵심 코드는 스칼라로 작성되어 있습니다. 스칼라는 함수형 프로그래밍과 객체지향 프로그래밍을 모두 지원하는 현대적인 다중 패러다임 프로그래밍 언어입니다. 스칼라는 JVM(Java Virtual Machine) 위에서 동작하므로 자바와 호환성이 좋습니다.

스파크는 또한 파이스파크(PySpark)라는 인터페이스를 통해 파이썬에서도 사용할 수 있습니다. 이는 데이터 과학자들이 파이썬으로 빅 데이터 처리를 수행할 수 있게 해주는 큰 장점입니다. 

 

 

설치과정

가장 간단하게는 그냥 pip install pyspark 하면 깔린다.

하지만, 먼저 설치한 하둡하고 연결하려면 아래 과정을 거치면 된다.

 

먼저 공식홈페이지에 가서 Pre-built for Apache Hadoop을 선택하고 다운로드를 받는다.

아래는 해당 다운로드 링크를 wget으로 받는 과정을 설명한다.

아래처럼 wget으로 받고 압축을 풀어준다.

cd /opt/spark
sudo wget https://dlcdn.apache.org/spark/spark-3.4.1/spark-3.4.1-bin-hadoop3.tgz
sudo tar -xvzf spark-3.4.1-bin-hadoop3.tgz

그다음 ~/.bashrc파일에 아래 3줄을 추가하고 source ~/.bashrc로 적용해준다.

export SPARK_HOME=/opt/spark/spark-3.4.1-bin-hadoop3
export PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin
export PYSPARK_PYTHON=/usr/bin/python3

그다음 설정파일에 대한 설정을 해준다.

cd /opt/spark/spark-3.4.1-bin-hadoop3/conf
cp spark-env.sh.template spark-env.sh

#spark-env.sh에 아래 두 줄 추가
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
export SPARK_HOME=$SPARK_HOME

#추가한 내용 반영
source spark-env.sh

그 다음 pyspark을 설치해준다.

pip3 install pyspark

 

 

설치가 잘 됐는지 테스트

다음 명령어를 통해 spark shell이 잘 뜨는지 확인(scala> 프롬프트가 뜨면 성공)

$SPARK_HOME/bin/spark-shell

다음 파이선 스크립트를 통해 하둡 HDFS와 잘 연동되는지 확인

from pyspark.sql import SparkSession

spark = SparkSession.builder.getOrCreate()
df = spark.read.text("hdfs://localhost:9000/test.txt")
df.show()
# 먼저 hdfs로 test.txt를 복사해준다.
$ cat test.txt
hello spark
$ hdfs dfs -put test.txt /

# 그다음 다음명령으로 test.py 수행
$ $SPARK_HOME/bin/spark-submit test.py
+-----------+
|      value|
+-----------+
|hello spark|
+-----------+

위의 spark-submit을 통한 실행시 test.py가 끝나기 전에 4040포트로 웹접속을 하면 다음과 같이 웹으로 경과확인이 가능하다!

반응형

'Programming > Linux' 카테고리의 다른 글

ELK연습  (0) 2023.07.30
스팍 - 실습 - 웹서버 세션분석  (0) 2023.07.30
하둡 - 실습 - 웹서버 세션분석  (0) 2023.07.29
하둡 - 실습 - 웹서버로그분석  (0) 2023.07.29
하둡(Hadoop)  (0) 2023.07.29

먼저 필요한경우 하둡을 포맷하고 초기화 해준다.

stop-dfs.sh  # 분산 파일 시스템(HDFS) 중지
stop-yarn.sh # 리소스 관리자(YARN)중지
hdfs namenode -format
start-dfs.sh
# YARN은 Hadoop의 주요 컴포넌트 중 하나로, 클러스터 리소스 관리 및 job scheduling을 담당하고 있습니다.
# 따라서, 만약 MapReduce 작업을 수행할 예정이라면, 이 명령어를 통해 YARN을 시작해야 합니다.
start-yarn.sh

# 나같은 경우 아래를 해야하는 경우가 있었다.
rm -rf /hadoop/data/*

# 헬스체크
hadoop fsck /

 

내 웹서버 로그 샘플은 다음과 같음

evity@sevityubuntu:~/workspace/hadoop_sevity.com/session_analysis$ cat ../iis_logs/* | head
#Software: Microsoft Internet Information Services 10.0
#Version: 1.0
#Date: 2015-09-06 08:32:43
#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
2015-09-06 08:32:43 127.0.0.1 GET / - 80 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/45.0.2454.85+Safari/537.36 - 200 0 0 211
2015-09-06 08:32:43 127.0.0.1 GET /iisstart.png - 80 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/45.0.2454.85+Safari/537.36 http://127.0.0.1/ 200 0 0 2
2015-09-06 08:32:43 127.0.0.1 GET /favicon.ico - 80 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/45.0.2454.85+Safari/537.36 http://127.0.0.1/ 404 0 2 2

이중에서 다음 컬럼을 뽑아서 map-reduce를 해보도록 하자.

날짜와 시간 (2015-09-06 08:32:43)
클라이언트 IP (c-ip)
요청된 URL (cs-uri-stem)

먼저 매핑과정을 통해 다음처럼 클라이언트 IP별로 30분 단위로 세션으로 묶어서 URL들을 뽑는다.

119.207.72.161  {"start_time": "2020-10-03 00:14:47", "end_time": "2020-10-03 00:14:48", "duration": 1.0, "url_list": ["/favicon.ico", "/board/dhtml_logo.js", "/menu_wonilnet.asp", "/", "/css/common.css"]}
66.249.65.54    {"start_time": "2020-10-03 00:27:31", "end_time": "2020-10-03 00:27:31", "duration": 0.0, "url_list": ["/wiki/doku.php"]}
80.82.70.187    {"start_time": "2020-10-03 01:59:09", "end_time": "2020-10-03 01:59:09", "duration": 0.0, "url_list": ["/cache/global/img/gs.gif"]}
139.59.95.139   {"start_time": "2020-10-03 02:11:31", "end_time": "2020-10-03 02:11:31", "duration": 0.0, "url_list": ["/"]}

그다음 리듀스 과정을 통해 IP별로 총 체류시간과 url_list를 묶어준다.

66.249.64.128   {"total_duration": 1694.0, "url_list": ["/menu_wonilnet.asp", "/menu_friend.asp", "/robots.txt", "/", "/menu_wonilnet_newImage.asp", "/favicon.ico"], "start_time": "2017-09-03 04:19:21", "end_time": "2018-07-29 20:43:11"}
66.249.70.30    {"total_duration": 1694.0, "url_list": ["/menu_wonilnet.asp", "/menu_friend.asp", "/robots.txt", "/wiki/doku.php", "/wiki/lib/tpl/dokuwiki/images/favicon.ico", "/", "/wiki/feed.php", "/wiki/lib/exe/js.php", "/wiki/lib/exe/css.php", "/css/common.css", "/wiki/lib/exe/detail.php", "/favicon.ico", "/wiki/lib/exe/fetch.php"], "start_time": "2017-07-26 01:31:14", "end_time": "2022-02-01 09:49:49"}
2.37.132.159    {"total_duration": 1689.0, "url_list": ["/"], "start_time": "2018-03-23 22:13:24", "end_time": "2018-03-23 22:43:34"}

폴더구조는 다음과 같으며, 이번 프로젝트의 현재 디렉토리는 session_analysis이다.

sevity@sevityubuntu:~/workspace/hadoop_sevity.com$ tree -L 1
.
├── iis_logs
├── ip_cnt
└── session_analysis

mapper.py를 다음과 같이 작성한다.

sevity@sevityubuntu:~/workspace/hadoop_sevity.com/session_analysis$ cat mapper.py
#!/usr/bin/env python3

import sys
import json
import codecs
from datetime import datetime, timedelta
import logging

logging.basicConfig(stream=sys.stderr, level=logging.ERROR)

previous_ip = None
url_list = []
start_time = None
end_time = None
session_timeout = timedelta(seconds=1800)  # 1800 seconds = 30 minutes

def reset_variables():
    global url_list, start_time, end_time
    url_list = []
    start_time = None
    end_time = None

# Using codecs to get stdin with a fallback in case of a UTF-8 decoding error
stdin = codecs.getreader('utf-8')(sys.stdin.buffer, errors='ignore')

for line in stdin:
    try:
        parts = line.strip().split()
        if len(parts) != 15:
            continue

        date_time = parts[0] + " " + parts[1]
        date_time = datetime.strptime(date_time, '%Y-%m-%d %H:%M:%S')

        ip = parts[8]  # changed from parts[2] to parts[8] to use the client IP
        url = parts[4]

        # If there is a previous ip and the current ip is different or a session timeout has occurred
        if previous_ip and (previous_ip != ip or (date_time - end_time) > session_timeout):
            print('%s\t%s' % (previous_ip, json.dumps({"start_time": str(start_time), "end_time": str(end_time), "duration": (end_time - start_time).total_seconds(), "url_list": list(set(url_list))})))
            reset_variables()

        if not start_time or date_time < start_time:
            start_time = date_time
        if not end_time or date_time > end_time:
            end_time = date_time

        url_list.append(url)
        previous_ip = ip

    except Exception as e:
        logging.error(f"Error processing line: {line.strip()}, Error: {e}")

# Print the last session
if previous_ip:
    print('%s\t%s' % (previous_ip, json.dumps({"start_time": str(start_time), "end_time": str(end_time), "duration": (end_time - start_time).total_seconds(), "url_list": list(set(url_list))})))

reducer.py를 다음과 같이 작성한다.

sevity@sevityubuntu:~/workspace/hadoop_sevity.com/session_analysis$ cat reducer.py
#!/usr/bin/env python3

import sys
import json

previous_ip = None
total_duration = 0.0
url_list = []
start_time = None
end_time = None

for line in sys.stdin:
    ip, session_info = line.strip().split('\t')
    session_info = json.loads(session_info)

    if previous_ip and previous_ip != ip:
        print('%s\t%s' % (previous_ip, json.dumps({"total_duration": total_duration, "url_list": list(set(url_list)), "start_time": start_time, "end_time": end_time})))

        total_duration = 0.0
        url_list = []
        start_time = None
        end_time = None

    if not start_time or session_info["start_time"] < start_time:
        start_time = session_info["start_time"]
    if not end_time or session_info["end_time"] > end_time:
        end_time = session_info["end_time"]

    total_duration += float(session_info["duration"])
    url_list.extend(session_info["url_list"])

    previous_ip = ip

if previous_ip:
    print('%s\t%s' % (previous_ip, json.dumps({"total_duration": total_duration, "url_list": list(set(url_list)), "start_time": start_time, "end_time": end_time})))

다음명령을 통해 map-reduce과정을 병렬로 진행한다.

hdfs dfs -rm -r /output && \
	hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar    \
    -file ~/workspace/hadoop_sevity.com/session_analysis/mapper.py     \
    -mapper 'python3 mapper.py'     \
    -file ~/workspace/hadoop_sevity.com/session_analysis/reducer.py     \
    -reducer 'python3 reducer.py'     \
    -input /logs/* -output /output \
    2>&1 | tee r.txt

결과를 HDFS에서 로컬파일시스템으로 가져온다.

rm -rf output && hadoop fs -get /output .

다음 python파일을 통해 duration역순으로 정렬한다.

import json
import sys

def get_duration(json_str):
    data = json.loads(json_str)
    return data.get('total_duration', 0)

lines = sys.stdin.readlines()

# 각 줄을 total_duration 값에 따라 정렬
lines.sort(key=lambda line: get_duration(line.split('\t', 1)[1]), reverse=True)

for line in lines:
    print(line, end='')
python3 sort.py < output/part-00000 > result.txt

result.txt를 보면 다음과 같다.

45.61.187.81    {"total_duration": 186353.0, "url_list": ["/"], "start_time": "2022-07-23 09:13:06", "end_time": "2022-08-22 20:57:09"}
127.0.0.1       {"total_duration": 161933.0, "url_list": ["/w/lib/tpl/dokuwiki/images/favicon.ico", "/wonilnet_pub/2010-06-09_192759_resize.png", "/w/lib/exe/css.php", "/image/top_menu_icon.gif", "/upgsvr/service.exe", "/favicon.ico", "/index.asp", "/w/lib/tpl/dokuwiki/images/pagetools-sprite.png", "/scrape", "/w/lib/tpl/dokuwiki/images/search.png", "/wiki/doku.php", "/w/lib/tpl/dokuwiki/images/usertools.png", "/", "/images/next5.gif", "/wonilnet_pub/2010-06-09_192656_resize.png", "/w/lib/tpl/dokuwiki/images/button-php.gif", "/image/rightconer_shadow.gif", "/menu_wonilnet_dbwork.asp", "/iisstart.png", "/w/lib/images/external-link.png", "/tc.js", "/w", "/css/common.css", "/a.php", "/images/num1_on_a.gif", "/w/lib/tpl/dokuwiki/images/button-donate.gif", "/w/lib/tpl/dokuwiki/images/button-html5.png", "/menu_friend.asp", "/w/lib/tpl/dokuwiki/images/button-dw.png", "/announce.php", "/w/doku.php", "/w/lib/tpl/dokuwiki/images/logo.png", "/announce", "/images/nonext.gif", "/images/num5_off.gif", "/image/garo_shadow.gif", "/scrape.php", "/image/sero_shadow.gif", "/w/lib/tpl/dokuwiki/images/button-css.png", "/images/num2_off.gif", "/menu_wonilnet.asp", "/favicon.png", "/image/leftconer_shadow.gif", "/images/num4_off.gif", "/images/num3_off.gif", "/image/line02.gif", "/board/dhtml_logo.js", "/w/lib/tpl/dokuwiki/images/page-gradient.png", "/w/lib/images/license/button/cc-by-sa.png", "/w/lib/exe/js.php", "/pub/02111401.jpg", "/w/lib/exe/indexer.php"], "start_time": "2015-09-06 08:32:43", "end_time": "2021-10-13 12:56:47"}
45.61.188.237   {"total_duration": 121316.0, "url_list": ["/"], "start_time": "2022-10-03 20:31:14", "end_time": "2022-10-26 20:10:54"}
64.62.252.174   {"total_duration": 102775.0, "url_list": ["/stock/test1/main.cpp", "/menu_wonilnet.asp", "/menu_friend.asp", "/stock/test1/id.cpp", "/robots.txt", "/wiki/lib/exe/css.php", "/wiki/lib/exe/opensearch.php", "/wiki/feed.php", "/wiki/doku.php", "/wiki/lib/exe/", "/wiki/lib/exe/indexer.php", "/wiki/lib/exe/js.php", "/wiki/lib/exe/mediamanager.php", "/wiki/", "/wiki/lib/exe/detail.php", "/wiki/lib/exe/fetch.php"], "start_time": "2022-08-11 04:54:40", "end_time": "2023-07-06 16:23:29"}

 

이 과정을 통해 다음과 같이 해킹을 시도하는 서버접근을 발견할 수 있었다.

124.173.69.66   {"total_duration": 1685.0, "url_list": ["/xw1.php", "/9510.php", "/link.php", "/sbkc.php", "/index.php", "/phpmyadmin/scripts/setup.php", "/xmlrpc.php", "/jsc.php.php", "/phpmyadmin3333/index.php", "/phpNyAdmin/index.php", "/hm.php", "/411.php", "/hhhhhh.php", "/key.php", "/92.php", "/pma/scripts/db___.init.php", "/aojiao.php", "/1/index.php", "/3.php", "/slider.php", "/plus/tou.php", "/g.php", "/ssaa.php", "/sss.php", "/lala-dpr.php", "/605.php", "/admin/index.php", "/plus/yunjitan.php", "/d.php", "/boots.php", "/win1.php", "/hell.php", "/uploader.php", "/images/vuln.php", "/error.php", "/que.php", "/099.php", "/aa.php", "/12345.php", "/666.php", "/api.php", "/plus/90sec.php", "/xiao.php", "/fusheng.php", "/xiaoxi.php", "/plus/ma.php", "/paylog.php", "/321/index.php", "/qwqw.php", "/qiangkezhi.php", "/liangchen.php", "/mysql.php", "/ganshiqiang.php", "/images/stories/cmd.php", "/xiong.php", "/datas.php", "/xixi.php", "/aaa.php", "/awstatstotals/awstatstotals.php", "/conflg.php", "/php2MyAdmin/index.php", "/think.php",

 

반응형

'Programming > Linux' 카테고리의 다른 글

스팍 - 실습 - 웹서버 세션분석  (0) 2023.07.30
스팍(Spark) 설치  (0) 2023.07.30
하둡 - 실습 - 웹서버로그분석  (0) 2023.07.29
하둡(Hadoop)  (0) 2023.07.29
Docker 설치, 초기설정, 명령어가이드  (0) 2023.07.17

어떤 IP에서 가장많이 접속했는지 확인

필자는 개인 IIS 웹서버를 운영하고 있음

다음을 통해 웹서버에서 하둡이 설치된 우분투 리눅스로 가져오고 HDFS로 복사(파일 복사가 RDB에서는 insert에 해당한다)

# 웹서버에서 하둡이설치된 리눅스로 로그파일들 복사
scp linowmik@secsm.org@192.168.0.6:/inetpub/logs/LogFiles/W3SVC1/* ./iis_logs

# HDFS로 복사
hdfs dfs -put ./iis_logs/* /logs/

# 위 복사가 너무 오래걸리면 아래 명령을 통해 진행상황 모니터링 가능
cd $HADOOP_HOME/logs
sudo tail -f $(ls -Art $HADOOP_HOME/logs | tail -n 1)
# 위명령어 설명은 다음과 같음
# ls -Art $HADOOP_HOME/logs: $HADOOP_HOME/logs 디렉토리 내의 파일을 날짜별로 오름차순 정렬합니다. -Art 옵션은 -A (숨겨진 파일 포함), -r (역순), -t (수정 시간별 정렬) 옵션을 동시에 사용하는 것입니다.
# tail -n 1: 파일 리스트 중에서 가장 최신의 파일 (리스트의 마지막 항목)을 선택합니다.
# tail -f: 선택한 파일의 내용을 실시간으로 출력합니다. -f 옵션은 'follow'의 약자로, 파일의 내용이 변경될 때마다 이를 반영하여 출력합니다.

로그가 어떤 형식인지 잠시 확인한다

sevity@sevityubuntu:~/workspace/hadoop_sevity.com$ cat iis_logs/*.log | head
#Software: Microsoft Internet Information Services 10.0
#Version: 1.0
#Date: 2015-09-06 08:32:43
#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
2015-09-06 08:32:43 127.0.0.1 GET / - 80 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/45.0.2454.85+Safari/537.36 - 200 0 0 211
2015-09-06 08:32:43 127.0.0.1 GET /iisstart.png - 80 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/45.0.2454.85+Safari/537.36 http://127.0.0.1/ 200 0 0 2
2015-09-06 08:32:43 127.0.0.1 GET /favicon.ico - 80 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/45.0.2454.85+Safari/537.36 http://127.0.0.1/ 404 0 2 2

#으로 시작하는 라인을 제외하면 날짜 시간 IP 등의 순임을 알 수 있다.

  • 2023-03-23 00:20:47 : 요청이 발생한 날짜와 시간.
  • 192.168.0.6 : 서버의 IP 주소 또는 호스트 이름.
  • GET : HTTP 요청 방법. 이 경우, 클라이언트는 서버로부터 정보를 요청하고 있습니다.
  • /wiki/doku.php : 클라이언트가 요청한 리소스의 경로.
  • id=learning_rate&do=login&sectok=bac058c83457f1bfade853577f509baf : 요청에 전달된 파라미터입니다.
  • 80 : 사용된 포트 번호. HTTP의 기본 포트는 80입니다.
  • - : 이 필드는 일반적으로 RFC 1413 ident 프로토콜을 통해 확인된 원격 사용자 이름을 나타냅니다. 여기서 "-"는 이 정보가 사용 불가능하다는 것을 의미합니다.
  • 216.244.66.237 : 클라이언트의 IP 주소.
  • Mozilla/5.0+(compatible;+DotBot/1.2;++https://opensiteexplorer.org/dotbot;+help@moz.com) : 사용자 에이전트. 이는 클라이언트가 사용하는 웹 브라우저 또는 봇을 설명합니다. 이 경우에는 DotBot이라는 봇이 서버에 요청을 보냈습니다.
  • - : 이 필드는 일반적으로 "referrer" URL을 나타냅니다. 여기서 "-"는 이 정보가 사용 불가능하다는 것을 의미합니다.
  • 200 : HTTP 상태 코드. 200은 성공을 의미합니다.
  • 0 : 서브 상태 코드. 이는 서버에 따라 다르게 해석될 수 있습니다.
  • 0 : Win32 상태 코드. 이는 서버에 따라 다르게 해석될 수 있습니다.
  • 1068 : 서버가 클라이언트에게 보낸 바이트 .

 

이를위한 mapper.py를 다음처럼 작성한다.

import sys
import logging

# Set up logging
# logging.basicConfig(filename="mapper.log", level=logging.DEBUG)
# 아래처럼 하면 하둡로그랑 섞여서 나와서 디버깅할때 좋다.
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)


# Ignore bad characters
for line in sys.stdin.buffer:
    try:
        line = line.decode('utf-8', 'ignore')
    except UnicodeDecodeError as e:
        logging.error(f"Error decoding line: {line.strip()}, Error: {e}")
        continue

    try:
        data = line.strip().split(" ")
        if len(data) == 15:
            date, time, s_ip, cs_method, cs_uri_stem, cs_uri_query, s_port, cs_username, c_ip, cs_user_agent, cs_referer, sc_status, sc_substatus, sc_win32_status, time_taken = data
            logging.debug(f"Client IP: {c_ip}, Count: 1")
            print("{0}\t{1}".format(c_ip, 1))
    except Exception as e:
        # 예외 발생 시 로그 기록
        logging.error(f"Error processing line: {line.strip()}, Error: {e}")

관심있는 IP와 카운트1을 결과로 출력함을 알 수 있다. (이러면 하둡이 스트리밍하여 Reduce쪽으로 넘긴다)

 

이제 아래와 같이 reducer.py를 작성하여 IP별 카운트를 집계한다.

#!/usr/bin/env python3

import sys
import logging

# Set up logging
logging.basicConfig(filename="reducer.log", level=logging.DEBUG)

last_key = None
running_total = 0
key = None

try:
    for input in sys.stdin:
        key, value = input.strip().split("\t", 1)
        if last_key == key:
            running_total += int(value)
        else:
            if last_key:
                logging.debug(f"Key: {last_key}, Running Total: {running_total}")
                print("{0}\t{1}".format(last_key, running_total))
            running_total = int(value)
            last_key = key

    if last_key == key and key is not None:
        logging.debug(f"Key: {last_key}, Running Total: {running_total}")
        print("{0}\t{1}".format(last_key, running_total))

except Exception as e:
    # 예외 발생 시 로그 기록
    logging.error(f"Error processing input: {e}")

다음명령을 통해 map-reduce를 수행(RDB에서 SELECT에 해당한다)

# 이전실행 클린업
hdfs dfs -rm -r /output

# 실행
hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \
	-file ~/workspace/hadoop_sevity.com/mapper.py \
    -mapper 'python3 mapper.py' \
    -file ~/workspace/hadoop_sevity.com/reducer.py \
    -reducer 'python3 reducer.py' \
    -input /logs/* -output /output 2>&1 | tee r.txt

그다음 결과를 hdfs에서 로컬로 가져와서 결과 보기

# 로컬로 가져오기
hadoop fs -get /output .

# IP카운트(2번째컬럼) 역순으로 정렬
sort -k2,2nr -t $'\t' output/* > sorted_output.txt

# 결과보기(상위 10개)
cat sorted_output.txt | head
216.244.66.237	69027
192.168.0.1	27738
211.189.165.105	20114
64.62.252.174	13246
119.207.72.84	8959
211.53.177.20	6565
66.249.82.88	5636
66.249.82.89	5632
66.249.82.90	5312
194.110.203.7	5157

# 가장 많이 접속한 IP에 대한 정보를 역추적해서 보기
grep -m 10 216.244.66.237 iis_logs/* | head
iis_logs/u_ex180725.log:2018-07-25 05:40:56 192.168.0.2 GET /robots.txt - 80 - 216.244.66.237 Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.opensiteexplorer.org/dotbot,+help@moz.com) - 404 0 2 499
iis_logs/u_ex180725.log:2018-07-25 05:44:26 192.168.0.2 GET / - 80 - 216.244.66.237 Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.opensiteexplorer.org/dotbot,+help@moz.com) - 200 0 0 641
iis_logs/u_ex180805.log:2018-08-05 02:39:00 192.168.0.2 GET /robots.txt - 80 - 216.244.66.237 Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.opensiteexplorer.org/dotbot,+help@moz.com) - 404 0 2 194
iis_logs/u_ex180805.log:2018-08-05 02:42:02 192.168.0.2 GET / - 80 - 216.244.66.237 Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.opensiteexplorer.org/dotbot,+help@moz.com) - 200 0 0 181
iis_logs/u_ex181027.log:2018-10-27 06:25:55 192.168.0.2 GET /robots.txt - 80 - 216.244.66.237 Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.opensiteexplorer.org/dotbot,+help@moz.com) - 404 0 2 183
iis_logs/u_ex181027.log:2018-10-27 06:35:36 192.168.0.2 GET / - 80 - 216.244.66.237 Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.opensiteexplorer.org/dotbot,+help@moz.com) - 200 0 0 176
iis_logs/u_ex181107.log:2018-11-07 01:04:26 192.168.0.2 GET /robots.txt - 80 - 216.244.66.237 Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.opensiteexplorer.org/dotbot,+help@moz.com) - 404 0 2 448
iis_logs/u_ex181107.log:2018-11-07 01:13:17 192.168.0.2 GET / - 80 - 216.244.66.237 Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.opensiteexplorer.org/dotbot,+help@moz.com) - 200 0 0 668
iis_logs/u_ex190201.log:2019-02-01 21:31:17 192.168.0.2 GET /robots.txt - 80 - 216.244.66.237 Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.opensiteexplorer.org/dotbot,+help@moz.com) - 404 0 2 118
iis_logs/u_ex190201.log:2019-02-01 21:45:12 192.168.0.2 GET / - 80 - 216.244.66.237 Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.opensiteexplorer.org/dotbot,+help@moz.com) - 200 0 0 118
sevity@sevityubuntu:~/workspace/hadoop_sevity.com$
반응형

'Programming > Linux' 카테고리의 다른 글

스팍(Spark) 설치  (0) 2023.07.30
하둡 - 실습 - 웹서버 세션분석  (0) 2023.07.29
하둡(Hadoop)  (0) 2023.07.29
Docker 설치, 초기설정, 명령어가이드  (0) 2023.07.17
vimdiff  (0) 2021.04.07

설치

하둡은 java기반이라 java sdk설치가 필요.

sudo apt-get update
sudo apt-get install default-jdk

그다음, 도커 이미지 받아서 그 안에서 연습해볼수도 있지만, 제대로 하려면 아래처럼 다운받고 설정하는 과정이 필요

# 하둡의 공식 웹사이트에서 최신 버전을 다운받아 설치
sudo wget https://dlcdn.apache.org/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz

# 압축풀고 설정진행
tar -xvf hadoop-3.3.0.tar.gz
sudo mv hadoop-3.3.0 /usr/local/hadoop
#~/.bashrc에 아래 것들 추가
export HADOOP_HOME=/usr/local/hadoop
export PATH=$PATH:$HADOOP_HOME/bin
export PATH=$PATH:$HADOOP_HOME/sbin
export HADOOP_MAPRED_HOME=$HADOOP_HOME
export HADOOP_COMMON_HOME=$HADOOP_HOME
export HADOOP_HDFS_HOME=$HADOOP_HOME
export YARN_HOME=$HADOOP_HOME
export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64


#반영
source ~/.bashrc

그다음 어떤 파일시스템을 하부로 쓸것인지 설정파일들을 좀 만져야함. 기본으로 깔렸을때는 hdfs가 아닌 로컬파일시스템을 쓰도록 되어 있음(;;)

# sudo vi $HADOOP_HOME/etc/hadoop/core-site.xml
# <configuration> 태그 사이에 아래내용 추가
<property>
    <name>fs.defaultFS</name>
    <value>hdfs://localhost:9000</value><!--이거 해줘야 로컬파일시스템이 아닌 hdfs씀-->
</property>

# sudo vi $HADOOP_HOME/etc/hadoop/hdfs-site.xml
# <configuration> 태그 사이에 아래내용 추가

<property>
    <name>dfs.replication</name>
    <value>1</value> <!--HDFS에 저장되는 각 데이터 블록이 클러스터 전체에서 복제되는 횟수를 결정-->
</property>

단일노드가 아닌 클러스터 구성시 추가 설정들이 필요(여기서는 생략)

 

다음은 포맷하고 전체시작

# 처음에 포맷해줘야 함
hdfs namenode -format

#전체 재시작
cd /usr/local/hadoop/sin
./stop-all.sh
./start-all.sh

#java ps. 아래처럼 NameNode, DataNode, SecondaryNameNode가 뜨는것을 확인
jps
128290 SecondaryNameNode
127865 NameNode
128026 DataNode

 

HDFS기본 핸들링 방법

아래처럼 hdfs dfs로 시작하는 명령을 주거나,  Hadoop이 제공하는 Hadoop FUSE(FIlesystem in USErspace) 모듈을 이용하면 기존파일시스템에 mount해서 ls,mv,cp등 그대로 사용하는 것도 가능

# /logs라는 폴더 만들기
hdfs dfs -mkdir /logs

# ls하기. Hadoop Distributed File System (HDFS)의 루트 디렉토리 내용을 나열
hadoop dfs -ls /
Found 1 items
drwxr-xr-x   - sevity supergroup          0 2023-07-29 13:20 /logs

# 파일 복사하기.
hdfs dfs -put iis_logs/* /logs

# 헬스체크
hdfs fsck /

 

 

트러블슈팅

기본적인 로그 모니터링

tail -f /usr/local/hadoop/logs/*.log

start-dfs.sh로 하둡을 시작했을때 namenode가 시작되지 않았을때(jps했을때 안보일때)

# 아래 명령으로 이유 파악
 grep -C 5 'ERROR' $HADOOP_HOME/logs/hadoop-sevity-namenode-sevityubuntu.log

# 만약 아래처럼 /tmp 아래 디렉토리 접근이 안된다는 것이면
2023-07-30 16:03:30,469 ERROR org.apache.hadoop.hdfs.server.namenode.NameNode: Failed to start namenode.
org.apache.hadoop.hdfs.server.common.InconsistentFSStateException: Directory /tmp/hadoop-sevity/dfs/name is in an inconsistent state: storage directory does not exist or is not accessible.
        at org.apache.hadoop.hdfs.server.namenode.FSImage.recoverStorageDirs(FSImage.java:392)

# hdfs-site.xml을 열어서 경로를 /tmp가 아닌 /var/lib등 임시가 아닌곳으로 옮겨준다.
vi /usr/local/hadoop/etc/hadoop/hdfs-site.xml
아래 내용 추가
    <property>
      <name>dfs.namenode.name.dir</name>
      <value>/var/lib/hadoop-hdfs/cache/hdfs/dfs/name</value>
    </property>

# 관련 폴더 만들고 권한 부여
sudo mkdir -p /var/lib/hadoop-hdfs/cache/hdfs/dfs/name
sudo chown -R sevity:sevity /var/lib/hadoop-hdfs/cache/hdfs/dfs/name

# 아래명령어로 다시 실행
stop-dfs.sh
start-dfs.sh

datanode가 시작되지 않을때

# jps 했는데 datanode관련된게 안보이면
grep -C 5 "ERROR" $HADOOP_HOME/logs/hadoop-*-datanode-*.log
# 아래와 같은 로그면
2023-07-30 16:24:23,819 ERROR org.apache.hadoop.hdfs.server.datanode.DataNode: Initialization failed for Block pool <registering> (Datanode Uuid 6b5e2ab5-f8a4-4eb8-b3d3-fe37de728e60) service to localhost/127.0.0.1:9000. Exiting.

# 다음처럼 /hadoop/data 안에 있는 데이터를 날려준다(데이터 포맷은 각오)
rm -rf /hadoop/data/*

부팅할때마다 hdfs 구동되도록 하기(기본적으로는 항상 start-dfs.sh로 시작해줘야함)

sudo vi /etc/systemd/system/hadoop-hdfs.service # 해서 아래내용 입력
[Unit]
Description=Hadoop HDFS
After=network.target

[Service]
User=sevity
Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
ExecStart=/usr/local/hadoop/sbin/start-dfs.sh
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

# 그 다음 다음을 입력해서 systemctl 리로딩하고, enable을 통해서 부팅시마다 시작되도록 함
sudo systemctl daemon-reload
sudo systemctl enable hadoop-hdfs

# 재부팅해서 jps해서 다음처럼 namenode, datanode, SecondaryNameNode 3가지 띁거 확인
jps
4673 SecondaryNameNode
3929 NameNode
4255 DataNode

# 마지막으로 헬스체크
hdfs fsck /

 

 

 

 

병렬처리 관련 중요 포인트

mapper가 병렬처리되는것으로 보이나 reducer는 모든 mapper가 끝나야 작업을 시작할 수 있나?
네, 맞습니다. MapReduce 모델에서 Mapper와 Reducer의 작업은 서로 다른 단계에 속하며, 이는 병렬 처리와 데이터 의존성 문제 때문입니다.
Mapper: 각 Mapper는 독립적으로 입력 데이터의 한 부분을 처리하며, 그 결과를 키-값 쌍의 형태로 출력합니다. 이 과정은 병렬로 진행될 수 있으며, 여러 컴퓨터 또는 프로세서 코어에서 동시에 일어날 수 있습니다.
Shuffle and Sort: Mapper의 출력은 모든 Reducer에게 보내지기 전에 "Shuffle and Sort" 단계를 거칩니다. 이 단계에서는 동일한 키를 가진 모든 데이터를 같은 Reducer로 라우팅하고, 키에 대한 데이터를 정렬합니다.
Reducer: Reducer는 키별로 그룹화된 데이터를 받아 처리합니다. 이 과정에서 Reducer는 자신에게 할당된 특정 키에 대한 모든 데이터를 받아야만 작업을 시작할 수 있습니다. 이는 각 키에 대한 모든 값들이 Reducer에서 합쳐지거나 처리되어야 하기 때문입니다. 따라서 Reducer는 모든 Mapper의 작업이 끝나야만 시작됩니다.
이와 같은 방식은 분산 환경에서 대규모 데이터를 처리하는 데 효율적인 방법을 제공합니다. Mapper는 독립적으로 실행되므로 컴퓨터 클러스터의 모든 머신을 활용할 수 있습니다. Reducer는 특정 키에 대한 모든 데이터를 처리하므로, 결과를 재결합하거나 요약하는 작업을 처리할 수 있습니다. 이 모델의 중요한 특성 중 하나는 개발자가 병렬 처리에 대한 세부 사항을 걱정할 필요가 없다는 것입니다. 이는 MapReduce 프레임워크가 처리하기 때문입니다.

반응형

여기를 참조했다.

아래 코드에서 find_intersection 함수를 라이브러리로 활용하자.

이 문제의 정답이기도 하다.

#include <bits/stdc++.h>
#define int long long
using namespace std;
#define REP(i,n) for(int i=1;i<=(int)(n);i++)

struct point { double x, y; 
    bool operator==(const point& other) const {return x == other.x && y == other.y;}
    bool operator<=(const point& other) const {return y < other.y || (y == other.y && x <= other.x);}
    bool operator>(const point& other) const {return y > other.y || (y == other.y && x > other.x);}
};
double CCW(point A, point B, point C, bool sign_only=true) {
    double r = (B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y);
    if (sign_only == false) return r;
    if (r == 0)return 0;
    return r > 0 ? 1 : -1;
}
struct line { point s, e; };
//touch_ok가 false이면, 두 선분이 교차하지 않고 만나기만 하는 경우에는 false를 리턴
bool Intersect(line x, line y, bool touch_ok=false) {
    point a = x.s, b = x.e;
    point c = y.s, d = y.e;
    double ab = CCW(a, b, c) * CCW(a, b, d);
    double cd = CCW(c, d, a) * CCW(c, d, b);
    if (ab == 0 && cd == 0) { // 이건 두 선분이 평행한 경우
        pair<double, double> aa = { a.x, a.y }, bb = { b.x,b.y }, 
            cc = { c.x, c.y }, dd = { d.x,d.y };
        if (aa > bb)swap(aa, bb);
        if (cc > dd)swap(cc, dd);
        if(touch_ok) return cc <= bb && aa <= dd; // 0이면 점끼리 만나는 것
        return cc < bb && aa < dd; // a<d이면서 b,c가 교차하면 선분
    }
    if(touch_ok) return ab <= 0 && cd <= 0; // 0이면 두 선분이 한점에서 만나는 것
    return ab < 0 && cd < 0; // 이게 기본. 각선분에서 나머지 2개점 방향이 달라야 교차
}

bool find_intersection(line l1, line l2, point& out) // 교점 구하기
{
    point A = l1.s, B=l1.e, C=l2.s, D=l2.e;
	if (A > B) swap(A, B);
	if (C > D) swap(C, D);
	double px = (A.x * B.y - A.y * B.x) * (C.x - D.x) - (A.x - B.x) * (C.x * D.y - C.y * D.x);
	double py = (A.x * B.y - A.y * B.x) * (C.y - D.y) - (A.y - B.y) * (C.x * D.y - C.y * D.x);
	double p = (A.x - B.x) * (C.y - D.y) - (A.y - B.y) * (C.x - D.x);

    bool found = false;
	if (p == 0) // 평행할 때
	{
		// 교점이 하나일 때
		if (B == C && A <= C) found=true, out = B;
		else if (A == D && C <= A) found=true, out = A;
	}
	else // 교차할 때
	{
		double x = px / p;
		double y = py / p;
        out = {x,y};
        found=true;
	}
    return found;
}


int32_t main()
{
    ios::sync_with_stdio(0); cin.tie(0);
    vector<line> l;
    double a, b, c, d; REP(i, 2)cin >> a >> b >> c >> d, l.push_back({ a,b,c,d });
    if(Intersect(l[0], l[1], true)==false) puts("0");
    else{
        puts("1");
        point intercection;
        bool found = find_intersection(l[0], l[1], intercection);
        if(found) printf("%.16lf %.16lf", intercection.x, intercection.y);
    }
    return 0;
}
반응형

+ Recent posts