[입개발] NAGLE 알고리즘과 TCP_CORK

어쩌다보니, 오늘 처음으로…(정말로 처음으로!!!) TCP_CORK라는 옵션에 대해서 찾아보게 되었습니다.(아니 이게 무엇이오 여러분!!!)

TCP_CORK라는 옵션을 설명하기 전에 먼저 TCP_NODELAY라는 옵션이 있습니다. 원래 데이터 전송의 효율성을 취하기 위해서 기본적으로 TCP 전송에 Nagle이라는 알고리즘이 적용되어 있습니다. 대용량 파일을 보낼 때는 유리하지만, 짧은 길이의 데이터를 보낼때는 사실 유용하지 않습니다. 네트웍 관련 서비스를 만들다 보면, 뭔가 응답이, 아무이유 없이, 아주 늦게 가는 케이스를 만나게 되는 경우가 있는데, 실제로, Nagle 알고리즘의 영향을 받아서 늦게 가는 경우가 종종 있습니다.

그러면 먼저 Nagle 알고리즘에 대해서 알아보면, 그냥 어느정도 데이터가 쌓일 때 까지 패킷을 보내지 않고 기다려 놓다가… 일정 사이즈가 되면 보내겠다라는 알고리즘입니다. 동네 버스가, 사람이 적을 때는 출발하지 않고, 몇명 와야 출발하는 것 처럼…(판교역 앞에 계시면 이런 경우를 많이 보시게 됩니다.)

이렇게 모아서 보내면 효율은 좋지만, 먼저 버스에 탄 사람은 인원 수가 모일때까지 가지 못하고, 기다려야 하는 단점이 있습니다. TCP 전송에서도 이게 그대로 발생합니다. 작은 패킷을 보내면 다른 패킷이 추가되어서 특정 사이즈가 되기 전까지는 전달이 되지 않습니다.(정확히는 send timeout 이 되면 전송됩니다.)

예전에 서비스를 운영하다보면, 마지막 2바이트가 몇초 뒤에 전송되어서 항상 문제가 되는 경우가 발생한 적이 있는데, 결국 해당 서비스는 TCP_NODELAY를 적용함으로써 해결했습니다.(또다른 문제의 시작일수도?)

앞에 Nagle을 이렇게 설명한 것을 잘생각해보면 DELAY가 생기는 거고, TCP_NODELAY는 바로 이 Nagle 알고리즘을 끄는 것입니다. 즉 패킷이 들어오면 바로바로 전송하는 거죠. 사실 여기까지가 제가 아는 Nagle 알고리즘이었습니다. 그런데 갑자기 TCP_CORK 가 딱!!!, TCP_CORK 는 Nagle과 유사한 알고리즘(?) 입니다.

https://stackoverflow.com/questions/22124098/is-there-any-significant-difference-between-tcp-cork-and-tcp-nodelay-in-this-use 해당 링크가 잘 설명이 되어 있는데, 요약하면 Nagle은 TCP_CORK의 약화버전이고, Nagle은 ACK를 체크하지만, TCP_CORK는 사이즈만 본다? 라는 뭔가 설명이 있는데…(그렇습니다. 저는 영어가…)

여기서 ACK는 TCP에서도 패킷을 보내고 나면 거기에 대한 ACK를 받게 됩니다. 혼잡제어나, 재전송이나… 그리고 사이즈라… 저 사이즈는 뭘까요. 패킷을 모아보내는 사이즈면 설정가능하지 않을까 하고 찾아보면, 따른 설정은 안보입니다. 네트워크 좀 아시는 분들은 아 그거 단위로 보내겠다고 쉽게 생각하시겠지만… 전 몰라요~~~

그럼 이제 커널 소스를 보면서 간단하게 생각해보도록 하겠습니다. net/ipv4/tcp_output.c 파일을 보면 tcp_write_xmit 라는 함수가 있습니다. 여기서는 다시 tcp_mss_split_point 를 호출합니다.

......
		limit = mss_now;
		if (tso_segs > 1 && !tcp_urg_mode(tp))
			limit = tcp_mss_split_point(sk, skb, mss_now,
						    min_t(unsigned int,
							  cwnd_quota,
							  max_segs),
						    nonagle);

......

tcp_mss_split_point 를 보면 needed 가 버퍼에 존재하는 패킷의 사이즈로 예측이 됩니다. 소켓 버퍼에 있는 사이즈와 window 사이즈 중에 적은게 선택이 됩니다. 그리고 max_len 이 needed 보다 작으면 max_len 이 전송이 되고, 중요한 부분은 partial 은 구하는 것입니다. 모듈러 하는 변수명이 보이시나요? 아까 말한 그 사이즈는 바로 mss_now 인 것입니다. 여기서 partial은 원래 네트웍에서 패킷을 MSS 단위로 보내기 때문에, 모듈러 mss_now 하면, mss가 1024일 때 우리가 600만 보낸다면, 424바이트가 MSS에 모자라기 때문에 partial 은 424 바이트가 됩니다. 코드를 보면 tcp_nagle_check 하고나서 true면 nagle을 적용해야 하는 상황일테니… needed – partial 만큼의 사이즈를 리턴합니다. 즉 MSS 단위로 패킷을 보내도록 짤라준거죠.

static unsigned int tcp_mss_split_point(const struct sock *sk,
					const struct sk_buff *skb,
					unsigned int mss_now,
					unsigned int max_segs,
					int nonagle)
{
	const struct tcp_sock *tp = tcp_sk(sk);
	u32 partial, needed, window, max_len;

	window = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
	max_len = mss_now * max_segs;

	if (likely(max_len len, window);

	if (max_len packets_out 이 0보다 커야 합니다.(보내는게 있다는 뜻으로...) 그리고 Nagle 알고리즘에서는 마지막으로 minshall 체크라는 걸 합니다. TCP_CORK는 별 다른게 없는데, 아까 Nagle과의 체크에서 ACK를 확인한다는 걸 기억하시나요?


static bool tcp_nagle_check(bool partial, const struct tcp_sock *tp,
			    int nonagle)
{
	return partial &&
		((nonagle & TCP_NAGLE_CORK) ||
		 (!nonagle && tp->packets_out && tcp_minshall_check(tp)));
}

minshall 이라는 분이 계시더군요(먼산…) 이 알고리즘은 그냥 보낸 패킷의 시퀀스와 ACK 받은 패킷의 시퀀스를 비교하기만 합니다. 아래 before 와 after는 그냥 before는 앞에 파라매터가 작으면 true, after는 앞에 파라매터가 크면 true 입니다. 저기서 snd_sml은 보낸 패킷의 시퀀스, snd_una는 ACK 받은 패킷의 시퀀스입니다. “!after(tp->snd_sml, tp->snd_nxt)” 이 코드는 시퀀스가 오버플로우 난 걸 확인하는 걸로 보입니다. 하여튼!!!, 즉 여기서 중요한 것은 ack를 다 받았다면 tp->snd_sml 과 tp->snd_una는 같은 값일 것이므로 false가 리턴됩니다. 즉 tcp_minshall_check가 true는 현재 ack 받아야할 패킷이 더 있다라는 뜻이고 false는 현재 모든 패킷의 ack를 받았다가 됩니다.

static inline bool before(__u32 seq1, __u32 seq2)
{
        return (__s32)(seq1-seq2) snd_sml, tp->snd_una) &&
		!after(tp->snd_sml, tp->snd_nxt);
}

그럼 요약을 하면 TCP_CORK는 이것저것 확인안하고 켜지면 무조건 MSS 단위로만 보내겠다가 됩니다. 그런데 Nagle은 전부 ACK를 받았다면 5 byte만 보낸다고 하더라도… ACK를 모두 받았으므로 tcp_minshall_check 가 false 가 되어서, 패킷이 보내집니다. 요약하면 mss가 1024이고 4100 바이트를 보낸다면 partial = 4100 % 1024 = 4, TCP_CORK에서는 마지막 4바이트는 전송이 되지 않습니다. 언제까지? timeout 이 발생할때까지…, 그러나 Nagle은… ACK를 받을 패킷이 남아있다면 마지막 4바이트가 전송이 안되지만, ACK를 모두 받았다면… 마지막 4바이트도 전송이 되게 됩니다. 이게 두 가지 옵션의 차이이고, Nagle이, TCP_CORK 보다 조금 약한 제약이라는 의미입니다.

Advertisements

[입 개발] spark-submit 시에 –properties-file 와 파라매터에서의 우선 순위

어쩌다보니… 갑자기 SparkSubmit 시에 사용되는 –properties-file(일종의 spark-defaults.conf)와 그냥 파라매터로 넘기는 것의 우선순위가 어떻게 적용되는지가 궁금해 졌습니다. 뭐, 당연히 일반적으로 생각하면 파라매터로 넘기는 것이 분명히 spark-defaults.conf 에 들어가있는 것 보다는 우선이 되는게 당연하겠지라는 생각을 가지고 있었고, 결론부터 말하자면, 이게 맞습니다.(다를 수가 없잖아!!! 퍽퍽퍽)

그러나, 우리는 공돌이니 그래도 명확하게 해두자라는 생각이 들어서, 소스를 가볍게 살펴봤습니다.
실제로 해당 내용은 “core/src/main/scala/org/apache/spark/deploy/SparkSubmitArguments.scala” 파일을 살펴보면 들어있습니다. 일단 main 코드는 다음과 같습니다. 여기서는 아주 간단히 확인할 것인데… 이름 부터 이미 parse 와 mergeDefaultSparkProperties 가 있습니다. 우리는 우선순위가 궁금할 뿐이니… parse 에서 가져온 것들을 mergeDefaultSparkProperties 에서 덮어쓸까만 확인하면 됩니다.

  parse(args.asJava)
  // Populate `sparkProperties` map from properties file
  mergeDefaultSparkProperties()
  // Remove keys that don't start with "spark." from `sparkProperties`.
  ignoreNonSparkProperties()
  // Use `sparkProperties` map along with env vars to fill in any missing parameters
  loadEnvironmentArguments()
  useRest = sparkProperties.getOrElse("spark.master.rest.enabled", "false").toBoolean
  validateArguments()

parse를 확인해 봅시다. 특별히 중요한 것은 없고 findCliOption 가 넘겨진 opts 중에서 해당 옵션이 있는지 확인하는 코드이고 handle 에서 실제로 해당 값을 셋팅하는 코드가 있습니다.

  protected final void parse(List args) {
    Pattern eqSeparatedOpt = Pattern.compile("(--[^=]+)=(.+)");

    int idx = 0;
    for (idx = 0; idx 
      val properties = Utils.getPropertiesFromFile(filename)
      properties.foreach { case (k, v) =>
        defaultProperties(k) = v
      }
      // Property files may contain sensitive information, so redact before printing
      if (verbose) {
        Utils.redact(properties).foreach { case (k, v) =>
          logInfo(s"Adding default property: $k=$v")
        }
      }
    }
    defaultProperties
  }

즉 defaultProperties -> sparkProperties 로 저장이 되는 겁니다. 그러면. 실제로 이 값의 우선순위는 어디에 저장이 되는가? 실제로 loadEnvironmentArguments 에서 해당 값이 설정이 됩니다. 아래에 보시면 Option에 먼저 executorMemory 가 NULL 이면 orElse 로 아까 저장한 sparkProperties 에서 가져오고 그래도 없으면 환경 변수에서 가져오고, 그래도 없으면 Null이 리턴됩니다.

  private def loadEnvironmentArguments(): Unit = {
    ......
    executorMemory = Option(executorMemory)
      .orElse(sparkProperties.get(config.EXECUTOR_MEMORY.key))
      .orElse(env.get("SPARK_EXECUTOR_MEMORY"))
      .orNull
    ......
  }

마지막으로 정리하면 결국 우선순위는 다음과 같습니다.

  1. 파라매터로 전달함 –executor-memory 이런식으로
  2. properties-file 로 저장한 값
  3. 환경변수

그런데 무조건 되는가에 대한 고민을 더 하셔야 합니다. 예를 들어 파라매터로 넘길 수 있는 것이 100%는 아닙니다. 다른 설정이 spark 설정 파일에 있을 수 가 있는 거죠. 즉 spark.yarn.executor.memoryOverhead 이런 값이 spark 설정 파일에 있다면, 여전히 이것 때문에 문제가 발생할 수 있다라는 것을 알아야 합니다.

[구글스터디잼] Kubernetes in the Google Cloud #3

해당 글은 현재 하고 있는 구글 스터디잼 Kubernetes in the Google Cloud 을 학습하는 과정에서 배우는 것을 정리하는 글입니다.

 * Setting up a Private Kubernetes Cluster
외부 IP를 가지고 않고 내부 네트워크 ip 대역만 가지고 동작하게 되는 쿠버네티스 클러스터를 설정하는 방법에 대해서 소개하는 세션입니다.

gcloud beta container clusters create private-cluster \
    --private-cluster \
    --master-ipv4-cidr 172.16.0.16/28 \
    --enable-ip-alias \
    --create-subnetwork ""

그리고 해당 클러스터 master에 접근할 수 있는 네트웍 대역을 지정할 수 있습니다. 여기서 MY_EXTERNAL_RANGE 는 CIDR 형태로 172.16.0.16/28 같은 형태로 표현해야 합니다.

gcloud container clusters update private-cluster \
    --enable-master-authorized-networks \
    --master-authorized-networks [MY_EXTERNAL_RANGE]

 

  • Helm Package Manager
    Helm 은 쿠버네티스 위에서 어플리케이션을 관리하는 매니저 툴입니다. 클라이언트/서버 구조로 구성되고 클라이언트는 Helm, 서버는 tiller 라고 부릅니다.

    Helm 의 설치는 다음과정을 통해서 이루어집니다.

    curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh
    chmod 700 get_helm.sh
    ./get_helm.sh
    kubectl -n kube-system create sa tiller
    

    그리고 tiller role을 설정해줍니다.

    kubectl create clusterrolebinding tiller --clusterrole cluster-admin --serviceaccount=kube-system:tiller
    

    tiller를 설치합니다.

    helm init --service-account tiller
    

    이제 거기에 올라가서 동작할 chart를 설치해봅니다.

    helm repo update
    helm install stable/mysql
    

  • NGINX Ingress Controller on Google Kubernetes Engine
    Ingress는 해당 노드로 들어오는 트래픽, Egress는 해당 노드에서 외부로 나가는 트래픽을 의미합니다. 여기서는 앞에서 배운 Helm을 이용해서, nginx 라는 좋은 웹서버를 이용해서 Ingress 트래픽을 제어하는 예를 보여주게 됩니다. Ingress 트래픽을 제외한다는 것은 특정 요청을 다른곳으로 보낸다거나, 바꿀수 있습니다.

ingress

먼저 kubenetes 클러스터를 생성합니다.

gcloud container clusters create nginx-tutorial --num-nodes 2

그리고 helm 을 인스톨합니다.

curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh
chmod 700 get_helm.sh
./get_helm.sh

Role based access control(RBAC) 설정을 합니다.

kubectl create serviceaccount --namespace kube-system tiller
kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'  

helm init --service-account tiller --upgrade

이제 hello-app 을 실행합니다.

 kubectl run hello-app --image=gcr.io/google-samples/hello-app:1.0 --port=8080
 kubectl expose deployment hello-app

이제 ingress 용 nginx를 실행합니다.

helm install --name nginx-ingress stable/nginx-ingress --set rbac.create=true

스크린샷 2019-01-27 오후 3.42.38

이제 위와 같이 nginx-ingress를 설정합니다.

kubectl apply -f ingress-resource.yaml

Reference

  1. https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/
  2. https://arisu1000.tistory.com/27835
  3. https://sktelecom-oslab.github.io/Virtualization-Software-Lab/Helm/
  4. https://kubernetes.io/blog/2016/10/helm-charts-making-it-simple-to-package-and-deploy-apps-on-kubernetes/
  5. https://bcho.tistory.com/1272

[구글스터디잼] Kubernetes in the Google Cloud #2

해당 글은 현재 하고 있는 구글 스터디잼 Kubernetes in the Google Cloud 을 학습하는 과정에서 배우는 것을 정리하는 글입니다.

3장 부터 4장은 Kubernetes 에서 Pod, Service, Scaling과 deployment 를 보여주고, 5,6장은 젠킨스와 slackbot 예제를 보여주고 있습니다.

실제로 이 예제를 따라가다 보면, 영어를 착실하게 보지 않는다면, Pod는 무엇이고 Service는 무엇인가에 대해서 막 혼란이 오게 됩니다.

Pod는 하나 또는 여러개의 컨테이너와 볼륨으로 구성된 오브젝트입니다. namespace 를 공유하고 Pod당 하나의 ip가 할당됩니다.(여기서 할당되는 ip는 public 일수도 있고, 그냥 private 일 수도 있습니다. 그러나, 보통 private 이 할당되겠죠?)

pods_1

그리고 Service는 특정 Pod들에 대한 endpoint 가 됩니다. 해당 Service의 endpoint 에 접속하면 아래의 Pod들에게 자동으로 데이터가 한번씩 전달되게 됩니다.(Load Balancer 역할)

service_1

예제를 살펴봐도, 하나의 Pod가 여러개의 container를 포함하지만, 하나의 Pod가 한 종류의 여러 Container를 가지는 걸로 보이지는 않고, 하나의 서버 단위가 하나의 Pod라고 봐도 될듯합니다. 예를 들어, 하나의 Pod에 API Server, Volume 가 들어가고, 이제 Service가 그 Pod들을 로드밸런싱 합니다. 서비스에 더 처리용량이 필요하면, Pod를 계속 추가하는 거죠. 이제 kubernetes의 scaling 이라고 보시면 됩니다.

아래 그림을 보면 kind 가 Pod로 나옵니다.
스크린샷 2019-01-20 오후 8.00.53

그리고 이 Pod 는 다음과 같이 실행이 가능합니다.

kubectl create -f pods/monolith.yaml

이제 이 앞단에 들어갈 서비스를 생성해봅시다.

스크린샷 2019-01-20 오후 8.00.40

생성하는 방법은 동일합니다.

kubectl create -f service/monolith.yaml

또, 논리적으로 구분하는 Namespace 라는 개념과, Label 이라는 개념을 이용해서 관리가 가능합니다. 해당 Pod는 frontend 다, backend다 이런식으로 태그를 달아주는 것이죠.

그리고 이런 오브젝트들을 관리하기 위해 컨트롤로러라는 개념이 있다고 합니다. 여기에 replicaset, deployment 라는 컨트롤러가 있고 이를 이용해서 쉽게 scale 변경이 가능합니다.

kubectl scale deployment hello --replicas=3
kubectl scale deployment hello --replicas=5

그리고 Rolling Update 도 지원합니다(한번에 정해진 단위로만 업데이트를 해서 전체를 업데이트 합니다.)
다음과 같이 edit deployment 를 이용하여 container 이미지를 수정하게 되면 자동으로 Rolling update가 시작합니다.

kubectl edit deployment hello

Rolling update를 멈추거나 멈춘 업데이트를 재개하고 싶다면 다음 명령을 이용합니다.

kubectl rollout pause deployment/hello
kubectl rollout resume deployment/hello

여기서 중요한 것은, 업데이트 이후에 이전 버전으로 돌리고 싶다면… rollout undo를 이용해서 이전 버전으로 쉽게 돌릴 수 있습니다. 이것도 Rolling update로 진행됩니다.

kubectl rollout undo deployment/hello

Reference
1. https://zzsza.github.io/development/2018/04/17/docker-kubernetes/
2. https://kubernetes.io/ko/docs/concepts/overview/working-with-objects/kubernetes-objects/
3. http://bcho.tistory.com/1256
4. https://blog.2dal.com/2017/03/07/kubernetes/

[구글스터디잼] Kubernetes in the Google Cloud #1

해당 글은 현재 하고 있는 구글 스터디잼 Kubernetes in the Google Cloud 을 학습하는 과정에서 배우는 것을 정리하는 글입니다.

총 10개의 챕터가 있고 일단은 가장 처음 두 개인, Introduction to Docker, Hello Node Kubernetes 를 공부했는데…

1] Introduction to docker
-> 실제로 Docker 책을 보면 쉽게 보게되는 명령들을 소개해줍니다. 즉 docker 를 실행시키거나, 현재 수행중인 container 에 접속한다거나, 현재의 수정본을 docker hub 등의 외부 registry에 등록하는 방법(사실 여기서는 gcloud 를 이용해서 gcp내의 registry에 등록하더군요.)

/node-app:0.2

사실 이미 docker 를 사용해 보신 경험이 있다면, 특별한 차이를 느끼지 못하실꺼 같습니다. 다른 차이는 실습을 하게 되는 환경에서 이미 docker/kubenetes 등이 다 설치되어 있어서… 아주 편하게 실습을 할 수 있다는 것 정도… 일단 제목이 Kubernetes in the Google Cloud 인거 처럼 kubenetes 내용은 2장 부터 시작됩니다.

2] Hello Node Kubernetes

Kubernetes 는 Container Orchestration Tool 이라고 볼 수 있습니다.(즉 컨테이너를 관리해주는?) 여기서는 간단히 kubenetes 위에서 node application을 배포하고 Rolling update 하는 것을 보여주게 됩니다.

2-1] Cluster 의 생성
다음과 같은 과정을 통해서 클러스터를 생성하게 됩니다. 클러스터가 생성된다고 이미지가 돌고 있거나 그러지는 않고, 클러스터만 실행된다고 생각하면 됩니다.

#project 이름 설정
gcloud config set project PROJECT_ID

#hello-world 라는 이름으로 cluster 생성, 노드는 2개, 머신 타입은 n1-standard-1, 생성되는 존은 us-centrall-a 입니다.
gcloud container clusters create hello-world \
                --num-nodes 2 \
                --machine-type n1-standard-1 \
                --zone us-central1-a


2-2] Pod 의 생성
생성된 클러스터에서 이제 실제로 container를 실행하게 됩니다.

#hello-node 라는 이름으로 컨테이너 실행
kubectl run hello-node \
    --image=gcr.io/PROJECT_ID/hello-node:v1 \
    --port=8080

#kubectl get deployments 로 현재 deployment 상황을 볼 수 있고
kubectl get deployments

#kubectl get pods 로 현재 수행중인 pod를 볼 수 있습니다.
kubectl get pods

2-3] container 외부에 노출하기
갓 만들어진 컨테이너를 외부에 서비스하기 위해서는 외부에서 접속을 할 수 있어야 합니다. 하지만 처음 생성되었을때는 kubenetes 내부에서만 연결이 되고 외부에서는 접속이 안될 것입니다. 이럴 때 특정 pod는 외부에서 접속할 수 있어야 하는데, 이러면 외부 IP를 가지거나 , 외부에서 해당 pod에 연결할 수 있는 proxy가 실행되어야 할껍니다.(아니면 기존 proxy의 설정이 바뀌거나…)

#외부에 노출하기
kubectl expose deployment hello-node --type="LoadBalancer"

#어떤 서비스들이 있는지 확인
kubectl get services
#결과의 EXTERNAL-IP를 통해서 외부 IP를 확인할 수 있습니다.

2-3] container scaling(개수 변경)
최초에는 container 가 하나만 실행되는데, 이 개수를 바꾸거나 하고 싶을때 어떻게 해야하는지 설명합니다.

#실행되는 컨테이너의 개수를 4개로 바꾸기
kubectl scale deployment hello-node --replicas=4

#kubectl get deployment를 하면 아까와 다른 결과를 볼 수 있습니다.

다만 이 과정과정가 눈깜짝할 사이에 끝나지는 않습니다. 저는 실험결과 몇초에서 몇분 정도 지나야 이제 제대로 적용되는 걸 볼 수 있습니다.

이제 쿠버네티스의 상태는 다음과 비슷합니다.
kube1

실제로 다음에 해보면 좋을것 같은 부분…
scale 은 실제로 서비스를 할 때, 중요한 부분이므로, 자신들의 서비스를 넣고, 롤링 업데이트나 scale을 바꿀때, 실제로 영향이 어떻게 되는지를 확인하시는게 좋을듯 합니다.

[입 개발] EXT4 에서 달라진 부분들 #2 – 데이터 영역의 관리

EXT4가 들어오면서 크게 달라진 부분 그 두 번째는, 데이터 영역을 관리하는 방법입니다. 일단 기존에서 사용하던 방식을 알아보도록 하겠습니다. 파일시스템의 특성을 볼때는, 그 시기에 있던 기술의 한계를 알면 도움이 되는데, 최초에 ext가 나오던 시기는 그렇게 큰 파일이 많지는 않던 시기입니다. 즉 엄청 큰 파일을 처리할 일이 많지는 않던 시기입니다. 그리고 기본적으로 ext2~3 까지는 하나의 파일의 최대 크기는 4 bytes 변수를 사용하기 때문에 최대 4기가 한계입니다.(블럭의 크기와 상관없이 일단 최대한 가졌을 때 표현할 수 있는 한계가 4기가입니다. 파일 크기를 나타내는 변수가 4 bytes 이기 때문이죠.)

여기서 보통 파일의 종류에 따라서 File/Directory 에서 File이면, 실제 데이터 영역에 파일의 내용이 있고, 디렉토리면, 해당 디렉토리에 있는 파일 목록에 대한 정보가 데이터 영역에 관리가 되게 됩니다. 즉, “I am a boy” 라는 내용을 가진 파일이 있다면, 그 파일의 크기는 10이고, block 크기가 4k라면, 한 개의 block만 할당이 되어있을것입니다. 그래서 데이터 영역에 가면 처음 10바이트가 “I am a boy”가 들어있게 됩니다. 이걸 관리하는 정보는 EXT는 inode에 있고, ext2~3에서는 그 크기는 총 60바이트입니다. 그리고 블럭을 가리키는 정보는 하나가 4 bytes 입니다. 즉, 60바이트의 공간이 할당되어 있는데, 한 칸이 4 bytes 면 총 15개의 정보를 저장하는 공간이 들어갈 수 있습니다.(FAT32 를 안다면 Fat Table 을 생각하시면 쉽습니다.)

그림1

그런데 여기서 잘 생각해보면, 그림1 처럼 한칸이 4 bytes라 하나의 블럭을 가리키면… 60바이트로는 총 15개… 한 칸이 하나의 블럭을 가리키므로, block 크기가 4k면 4k * 15 = 60k 밖에 안됩니다.(아까는 4기가가 최고라며!!!) 어떻게 된 것일까요? 사실 EXT2/3의 특징 중에 하나가 direct, indirect block, double indirect block, triple indirect block 으로 구성이 된다는 것입니다.

먼저 시작하기 전에 inode에 들어있는 15 개의 칸은… 12개의 direct와 각각 하나씩의 indirect, double indirect, triple indirect 로 구성이 되어있습니다. 먼저 direct 는 그 칸 하나가 그림2처럼 실제 디스크의 블럭하나를 가리키는 것입니다. direct 로는 최대 60k 밖에 파일이 가질 수가 없으므로, 이 사이즈를 더 크게 사용하기 위한 것들이 위한 indirect, double indirect, triple indirect 방식입니다.

그림2

12개니 각 블럭이 4k면 48k의 공간을 지정할 수 있습니다. 그렇다면 13번째 칸의 indirect 는 뭐냐, indirect 가 가리키는 블럭으로 가면, 아까 12개의 direct block을 가리키는 정보가 있던 것과 같은 형태로 실제 디스크를 가리키는 블럭이 direct block이 나옵니다. 블럭 크기가 4k면 여기에 실제 디스크의 블럭을 가리킬 수 있는 정보가 각각 4 byte니 1024개가 들어가게 됩니다. 즉, indirect block은 실제로 direct 블럭 1024개를 가리키므로 4M의 공간을 지정할 수 있습니다. 그림3을 참고합시다.

그림3

그럼 이제 슬슬 이해가 가기 시작할 것입니다. indirect 블럭은 해당 블럭이 direct block을 어드레싱 하는 주소 정보가 들어가 있고, double indirect 블럭이 가리키는 정보는 indirect 블럭을 가리키는 블럭 1024개를 가리키는 블럭에 대한 정보가 됩니다. 그림4를 참고하면, 실제로 double indirect 블럭의 내용은 indirect block 정보들입니다.

그림4

여기서는 4M 짜리 1024개를 가리키므로 4G 의 디스크 정보를 가리킬 수 있습니다. 이쯤되면 이제 triple indirect 의 성격도 눈치 채실 껍니다.
triple indirect block이 가리키는 정보는 double indirect block 1024개를 가리키는 정보입니다. 그림5를 참고하면 됩니다. 그런데 여기서
의아한 것은 사실 double indirect block 만으로도… 실제 파일의 최대 크기인 4G가 넘어간다는 것입니다.(물론 블럭 크기가 4K 일 경우입니다. 1K로 설정되면 이 마지막 블럭을 사용하게 되겠지만… 블럭 크기가 4k면… 마지막 triple indirect 는 사용할 이유가 없습니다.)

블럭 크기가 4k 일때, triple indirect block은 4G * 1024 = 4TB 의 데이터 영역을 가리킬 수 있습니다. 그림5를 참고합시다.

그림5

그런데 ext4로 가면서 왜 이런 구조를 버리는 것일까요? 간단하게 생각해보면 해당 구조는 낭비가 너무 심합니다. 혹시나 FAT32에서 NTFS로 가면서 내부구조가 어떻게 바뀌었는지 이해한다면, 실제로 EXT4의 extend를 이해하는 것은 굉장히 수월합니다.

먼저 간단하게 생각해봅시다. 1000, 1001, 1002, 1003 4개의 블럭이 있다고 할 때 이걸 가리키는 방법으로 ext2에서의 기존 방식은 하나에 한칸을 가리켜야 하므로 총 4칸 16 바이트가 필요합니다. 그런데 이렇게 표현할 수 있지 않을까요? (시작위치, 블럭 개수), 이런 형태면 다음과 같은 (1000, 4) 형태로, 표현이 가능해집니다. 데이터 공간도 각각 4 바이트를 쓴다고 하더라도 8바이트면 줄어듭니다. 물론 매번 필요한 공간이 Fragmentation 이 발생한다면 (1000, 1), (1001, 1), (1002, 1), (1003, 1) 형태로 공간을 낭비하게 됩니다. 즉 연속된 공간이 많이 필요할 수록, 새로운 표현 방법이 공간을 절약할 수 있습니다. 또한 추가 분석할 필요 없이도 쉽게 뒤에 얼마만큼 읽어야 할지도 알 수 있게 됩니다.

이런 방식을 적용한것이, ntfs의 cluster runs 나, ext4의 extent 라는 구조입니다. 그런데 extent는 (시작위치, 블럭 개수) 의 구조에서 블록 개수에는 2 byte만 할당되어 있습니다. 그래서 block 크기가 4k 기준일 때 무조건 32768 즉 4k * 32768 = 128MB 이상되면 아무리 디스크에 연속적으로 할당된 공간이 있더라도 extend가 추가로 생기게 됩니다. 즉 1GB 파일이면 8개는 extent가 생겨야 합니다.

그런데 extent구조를 보면 헤더가 일단 12 바이트이고, 리프 노드(실제 파일의 정보를 가리키는)냐, 인덱스 노드(리프 노드나 다른 인덱스 노드의 정보를 가지는)냐에 따라 가지는 정보들이 각각 12 bytes입니다. 즉… 60바이트에서 헤더를 빼고나면 일단 inode 안에는 4개의 리프 노드만 들어갈 수 있습니다. 즉 512MB 보다 파일 사이즈가 커지거나 fragmentation 이 많이 나면… 결국 tree 형태로 관리되는 정보가 실제 다른 블럭에 들어가야만 합니다.

그림6

그래서 실제 구조는 그림6처럼 되게 됩니다. 그리고 이 extent도 실제로는 해당 파일의 inode의 flag에 USE_EXTENT 가 설정되어 있어야 사용하게 됩니다. 즉 ext4 내에 있는 파일이 어떤 건 extent 형태로, 어떤건 옛날 방식으로도 주소 지정이 가능하다라는 것입니다. 다음번에는 기존에 왜 한 디렉토리에 파일이 많으면, 파일 열기등이 느려지는지, ext4에서는 어떻게 풀고 있는지 가볍게 살펴보도록 하겠습니다.

[입 개발] EXT4 에서 달라진 부분들 #1 – Flexible Block Group

사실 EXT4는 나온지 굉장히 굉장히 오래된 파일 시스템입니다. EXT의 역사를 보면 간략히 다음과 같습니다.

  • EXT, 1992년 출시
  • EXT2, 1993년 출시
  • EXT3, 2001년 커널 2.4.15에 포함
  • EXT4, 2006년 커널 2.6.19에 포함
  • 2008년 커널 2.6.28에 EXT4 안정 버전 포함

한마디로 굉장히 오래되었습니다. 거의 10년이 지났네요. 그런데 아는 분들은 다 아시지만, 그래도 모르는 사람들은
EXT2 -> EXT3 -> EXT4로 넘어가면서 무엇이 변했는지 잘 모릅니다. 그냥 좋아졌다?

EXT4로 들어오면서 크게 바뀐것들이 좀 있는데, 그 중에서도 Flexible Block Group, Extent를 위한 HTree,
많은 수의 파일 목록에서 쉽게 파일을 찾기 위한 Hash Directories 등이 있습니다.

일단 생각은 해당 EXT4 시리즈에서는 크게 위의 세 가지만 다뤄볼려고 합니다. 자세한 것은 당연히 다루지 않습니다.
왜냐… 잘모르니깐…

Flexible Block Group 을 설명하기 위해서는 먼저 EXT에서 Disk Layout 을 어떻게 구성하는가 부터 알아야
합니다. EXT에서는 Block Group 이라고 해서 특정 사이즈 단위로 디스크를 나눠서 관리 정보를 가지고 있다고
보시면 됩니다.

그림1

즉 그림1 처럼 하나의 디스크를 여러개의 Block Group 으로 나누게 됩니다. 보통 하나의 Block Group 은 128MB
할당 됩니다. 그럼 하나의 Block Group 은 어떻게 구성되는가? 그림2 처럼 각 Block Group 은 전체 정보를 가지는
Super Block 과 GDTs, Reserved GDTs를 가지게 됩니다. 이 정보들은 주로 Block Group 0번의 것을 사용하지만
백업용으로 다른 Block Group 들에도 존재합니다. 이 것 이외에 Block Bitmap, Inode Bitmap, Inode Table,
Data 영역은 각 Block Group 단위로 관리되는 정보들입니다. 이 때 보면 Block Bitmap, Inode Bitmap은 하나의
Block 만 할당 받기 때문에 Block의 크기가 4K 라면 bit 단위로 총 32k 개를 관리할 수 있습니다. 그래서 32k * 4K
해서 하나의 Block Group 의 크기가 128MB로 보통 설정되게 됩니다.

그림2

그런데 이 구조의 단점은 무엇일까요? 작은 크기의 파일은 큰 문제가 없는데, 128MB(실제로는 이것보다 조금 더 작은 크기의)
를 넘어가는 파일부터는 Block Group 하나에 저장되지 않아서, 무조건 Disk 에 물리적으로 Fragmentation 이 발생하게 됩니다.
즉 연속적으로 읽을 수 없고, Random Access 가 발생해야 한다는 것이죠. 그리고 현재에는 사실 128MB 단위는 생각보다
많이 작습니다.

그럼 결국 Flexible Block Group 은 뭐냐? 몇개의 Block Group 들을 하나로 묶자는 것이 메인 아이디어 입니다.
그래서 그림3 처럼 각 Block Group 의 Block Bitmap, Inode Bitmap 등이 쭈욱 연결되서 하나처럼 관리되게 됩니다. 여기서는
8개의 Block Group 이 하나처럼 관리되는 것으로 가정했습니다.

그림3

정리하자면, 각 Block Group 에 있던, Block Bitmap, Inode Bitmap, Inode Table 영역들이 쭈욱 연결되는 형태로 들어가게 됩니다.
0번 Block Group 에 다른 Block Group 들의 메타정보(Block Bitmap, Inode Bitmap, Inode Table) 들이 다 넘어오게 되는 것이죠.
그림2와 비교해보시면 그 차이가 명확하게 보이실 겁니다.

주의 할 것은 EXT4에서 Flexible Block Group 은 무조건 사용하는 것이 아니라, 헤더에 FLEX_BG 라는 Flag가 설정되어 있어야만 Flexible Block Group 을 사용 가능합니다. 즉, 이 Flag가 꺼져있다면, 예전 방식대로 처리해야한 합니다.

다음번에는 EXT4에서 extents를 어떻게 관리하는지, 살펴보도록 하겠습니다.

EXT4 에 대해서 Ext4 Disk Layout 이라는 매우 좋은 자료가 있습니다. 자세한 건 이걸 참고하세요.