레이블이 argo-workflows인 게시물을 표시합니다. 모든 게시물 표시
레이블이 argo-workflows인 게시물을 표시합니다. 모든 게시물 표시

vector memory growth issue in argo-workflow

k8s 환경의 특정 노드에서 동작중인 vector pod 1개가 메모리가 계속증가하고 있었다.
이유는 argo workflow 노드1개에만 스케쥴링되어서다.

[스케쥴링 원인]
argo-workflow(controller) 가 직접 스케줄링 하는 게 아니라 k8s 기본 스케줄러 (kube-scheduler) 가 워크플로우 task 파드를 노드에 배치한다.
workflow spec 에 nodeSelector/affinity 가 없어 다음 스케줄러의 기본 점수 두 개로 결정된다.
1. NodeResourcesFit(과거 k8s v1.22 이전 NodeResourcesLeastAllocated, NodeResourcesMostAllocated, RequestedToCapacityRatio 등이 통합됨)
- 각 노드의 요청(request) 점유율이 가장 낮은 노드에 가장 높은 점수.
- 당시 CPU request worker01 34% / worker02 35% / worker03 25% <- 최저 / worker04 30% / worker05 30%
- 그래서 새로 들어오는 워크플로우 task 가 매번 worker03 으로 떨어짐.

2. ImageLocality
- 노드에 이미 캐시된 이미지를 쓰는 파드 에 추가 점수.
- 첫 워크플로우가 worker03 으로 떨어지면 그 노드에 이미지가 캐시 -> 다음 워크플로우도 worker03 이 더 높은 점수 -> 눈덩이 효과.

결과적으로 워크플로우 task 생성되면 -> kube-scheduler 점수 계산 -> worker03 선택 -> 이미지 더 강하게 캐시됨 -> 다음 task 도 같은 결과 -> 누적

[vector 로그 대상]
vector 의 kubernetes_logs source 는 pod들을 watch 한다.
pod 가 살아있으면 vector 는 그 파드를 수집 대상으로 계속 인식하고 다음을 메모리에 들고 있는다.
- 파드 메타데이터 캐시: namespace / pod_name / container_name / labels / annotations / node_name 등 100여 개 누적되면 무시 못할 양.
- file watcher state: 각 컨테이너 로그 파일의 inode, 마지막 read offset(checkpoint), open file descriptor.
- glob rescanning: config 에 glob_minimum_cooldown_ms: 8000 이라 8초마다 /var/log/pods/** 를 다시 스캔. 완료된 파드 디렉토리가 매번 결과에 잡혀 비교 작업이 일어남.

[조치1]
ns:argo-workflow > cm:workflow-controller-configmap > 아래 내용을 설정한다.
data:
  config: |
    workflowDefaults: # 각 워크플로가 별도의 설정이 없다면 사용될 디폴트 설정
       spec:
         podGC: # 워크플로우 task 파드 정리 전략1 - 상태 기반
           strategy: OnWorkflowSuccess # 워크플로우 전체가 성공으로 끝났을 때 task 파드들을 일괄 삭제. 실패하면 파드를 남겨 두어 사후 디버깅.
           deleteDelayDuration: 1m # strategy 가 발동한 뒤 1분 대기 후 실제 삭제. 로그 수집기(vector 등) 가 마지막 라인을 읽어 Kafka 까지 보낼 시간 확보.
         ttlStrategy: # 워크플로우 task 파드 정리 전략2 - 시간 기반
           secondsAfterSuccess: 3600 # 성공한 Workflow CR 을 1시간 뒤 삭제.
           secondsAfterFailure: 86400 # 실패한 Workflow CR 은 24시간 보존 후 삭제.

podGC + ttlStrategy 로 스케줄링 노드 편중을 막진 않지만 누적을 끊어 영향 자체를 무력화시킨다.
podGC , ttlStrategy 는 둘중 하나만 만족되면 정리가 되네 일정기간 유지하고 싶다면 podGC는 빼고 ttlstrategy 만 사용해야 한다.

argo workflows v2.4+ 부터 controller 가 ConfigMap 변경을 watch 해서 자동 reload 해서 별도 재시작이 필요 없다.
workflowDefaults(podGC/ttlStrategy)는 신규 워크플로우 부터만 적용된다.

[조치2]
이미 진행 중이거나 완료된 워크플로우에는 적용 안되니 작업이 끝난 workflow pod들을 종료한다.
kubectl -n argo-workflow delete pod --field-selector=status.phase=Succeeded
kubectl -n argo-workflow delete pod --field-selector=status.phase=Failed

[조치3]
vector 는 rust 로 작성되어 있고 jemalloc 을 기본 할당자로 사용한다.
jemalloc 은 한번 so 로 부터 받은 메모리를 내부 free pool 로 두고 잘 OS 로 돌려주지 않는다.
즉 vector 가 내부적으로 이제 안 쓰는 메모리가 됐어도 프로세스 RSS / working set 은 그대로 보인다.
새로 들어오는 워크로드는 이 free pool 을 재활용하므로 메모리는 더 늘지 않지만, 줄어들지도 않는다.
이런 이유로 메모리가 증가된 vector pod1개는 새로띄운다.(vector daemonset 이라 pod 삭제하면 해당노드새 새 vector pod 생성된다.)
kubectl -n vector delete pod vector-abc123

사실 개별 workflow 는 ttl 설정이 되어 있어 자동 정리(삭제)가 되는데, cronworkflow 부분이 빠져 있어 문제가 됐다.

argo-workflow hypen name

# argoworkflow 3.1 이후 Expression Templates ({{= ... }}) 에서 step 이나 parameter 이름에 -  이 있는 경우 steps.aaa-bbb-ccc 는 사용하면 안되고 steps["aaa-bbb-ccc"] 로 사용해야 한다.

# 다음과 같이 step 동작을 테스트
# argo_workflow_steps_test.yaml
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  name: ysoftman-wf
  namespace: ysoftman-workflow
spec:
  serviceAccountName: ysoftman-workflow
  entrypoint: ysoftman-wf
  templates:
    - name: aaa
      script:
        image: alpine
        command: [sh]
        source: |
          echo "---"
      outputs:
        parameters:
          - name: err-msg
            value: "ERROR!!!"

    - name: bbb
      inputs:
        parameters:
          - name: param1
      script:
        image: alpine
        command: [sh]
        source: |
          echo '{"code":400,"msg":"에러({{ inputs.parameters.param1  }})","desc":"test"}' > /tmp/response.json
      outputs:
        parameters:
          - name: response
            valueFrom:
              path: /tmp/response.json

    - name: ccc
      inputs:
        parameters:
          - name: param1
      script:
        image: alpine
        command: [sh]
        source: |
          echo '---> {{ inputs.parameters.param1 }}' > /tmp/response.json
      outputs:
        parameters:
          - name: response
            valueFrom:
              path: /tmp/response.json

    - name: ysoftman-wf
      steps:
        - - name: aaa-job
            template: aaa
        - - name: bbb-job
            template: bbb
            arguments:
              parameters:
                - name: param1
                  # value: "{{steps.aaa-job.outputs.parameters.err-msg}}"
                  # {{=...}} expression 식으로 사용하는 경우(https://argo-workflows.readthedocs.io/en/latest/variables/#expression)
                  value: "{{=steps['aaa-job'].outputs.parameters['err-msg']}}"
        - - name: ccc-job
            template: ccc
            arguments:
              parameters:
                - name: param1
                  # value: '{"message":"ysoftman-wf {{steps.bbb-job.outputs.parameters.response}}"}'
                  # {{=...}} expression 식으로 사용하는 경우(https://argo-workflows.readthedocs.io/en/latest/variables/#expression)
                  # single quote 이스케이프 처리시 '' 로 두번쓰면 된다.
                  # value: '{"message":"ysoftman-wf {{=steps[''bbb-job''].outputs.parameters[''response'']}}"}'
                  value: '{"message":"ysoftman-wf {{=replace(jsonpath(steps[''bbb-job''].outputs.parameters[''response''], "$.msg"), "!","~")}}"}'

# 접속 환경 설정
export ARGO_SERVER='argo-workflow.ysoftman.test:80'
export ARGO_HTTP1=true
export ARGO_SECURE=false
export ARGO_BASE_HREF=""
export ARGO_TOKEN="Bearer $(kubectl get secret argo-workflows -n argo-workflows -o=jsonpath='{.data.token}' | base64 --decode)"
export ARGO_NAMESPACE="argo-workflows"

# wf 실행
argo submit ./argo_workflow_steps_test.yaml

# wf 삭제
argo delete ysoftman-wf

# 테스트 결과
# bbb-job input/output

# ccc-job input/output

argo workflow onexit and hooks

# argo workflow 에서 워크플로우의 종료시 보통 Spec.OnExit 를 사용한다.
# 사용예시 https://github.com/argoproj/argo-workflows/blob/main/examples/exit-handlers.yaml
spec:
  entrypoint: intentional-fail
  onExit: exit-handler

# OnExit 핸들러는 workflow.status 로 성공,실패 여부를 판단한다.
# 참고로 steps 에서 - 에 개수에 따른 차이
# - - 는 이전 단계 이후에 순차적으로 실행
#   - 는 이전 단계와 동시에 실행(argo workflow 그래프로 보면 스텝(노드)들이 동시에 수행되는 것으로 표시된다.)
  - name: exit-handler
    steps:
    - - name: notify
        template: send-email
      - name: celebrate
        template: celebrate
        when: "{{workflow.status}} == Succeeded"

# 그런데 steps.OnExit 로 설정하면 workflow.Status 가 "Running" 로 나온다.
spec:
  template:
  steps:
  - - name: aaa
      onExit: exit-handler

# 주석을 자세히 보니
# spec.onExit 에서는 primary workflow 의 workflow.status(global변수) 가(success, failure, error) 인지를 알 수 있다.
# step.onExit 은 아직 워크플로우가 진행중이니 running 상태가 되는것으로 보인다.
# 아래 코드를 보면 step.OnExit 는 LifecycleHook 형태가 된다.

# 참고로 workflow.status 에 다음과 같은 phase 값이 올 수 있다.
(Unknown)
Pending
Running
Succeeded
Failed
Error

# hooks 는 workflow level, template 에서 사용할 수 있다.
# hooks.exit 는 onExit 와 같은 역할을 한다.
# 사용예시 
spec:
  entrypoint: main
  templates:
  - name: main
    steps:
      - - name: aaa
          hooks:
            running:
              expression: steps.aaa.status == "Running"
              template: http
            success:
              expression: steps.aaa.status == "Succeeded"
              template: http
            failed:
              expression: steps.aaa.status == "Failed"
              template: http
          template: echo

#####

# hook 은 현재 단계의 template 시작에 한번만 트리거돼 스텝별 시작/종료 상태를 파악하기 힘들다.
# 현재 단계에 시작/종료시 상태 파악을 위해 별도 template 을 앞뒤로 넣어준다.
specs:
  entrypoint: main
  templates:
  - name: main
    steps:
    - name: start-main
      template: update-status
      arguments:
        parameters:
        - name: task
          value: "start-main"
        - name: status
          value: "running"
  
    - name: main
      template: echo
        
    - name: finish-main
      template: update-status
      arguments:
        parameters:
        - name: task
          value: "finish-main"
        - name: status
          value: "{{steps.main.status}}"

# 상태 처리를 위해 호출될 템플릿
  - name: update-status
    inputs:
      parameters:
      - name: task
        value: ""
      - name: status
        value: ""
    steps:
    - - name: running-status
        when: "{{inputs.parameters.status}} == running"
        template: write-status
      - name: success-status
        when: "{{inputs.parameters.status}} == Succeeded"
        template: write-status
      - name: fail-status
        when: "{{inputs.parameters.status}} == Failed"
        template: write-status

#####

# 참고
# golang template 에서 사용한다면 다음같이 backtick 으로 감싸줘야 한다.
"{{ `{{inputs.parameters.status}}` }}"

# 팁
# vim 등에서 argo workflow yaml 에 대한 lint 가 없어 편집시 불편하다.
# workflow yaml 작성시 다음과 같이 hwatch(watch 대체 커맨드)와 argo lint 로 확인하면서 편집하자.
hwatch -n 1 argo lint workflow.yaml

argo-workflow parameter json escape

# argo-workflow parameter 로 json 형태를 전달 받았을떄
# single quotation 으로 감싸지 않으면 " escape 가 되지 않는다.

# test 
# argo worfklow version: 3.4.4
# workflow
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: ysoftman-test
  namespace: ysoftman-workflow
spec:
  serviceAccountName: workflow-template
  entrypoint: ysoftman
  templates:
    - name: ysoftman
      steps:
        - - name: ysoftman
            template: print-data
            arguments:
              parameters:
                - name: data1
                  value: "{\"a\":\"apple     lemon\"}"
                - name: data2
                  value: '{"b":"banana"}'

    - name: print-data
      inputs:
        parameters:
          - name: data1
          - name: data2
      script:
        image: "alpine"
        command: [sh]
        source: |
          echo '{{inputs.parameters.data1}}'
          echo '{{inputs.parameters.data2}}'
          echo '{{inputs.parameters}}'
          # invalid JSON 
          echo "{{inputs.parameters.data1}}"
          echo "{{inputs.parameters.data2}}"
          echo "{{inputs.parameters}}"
          # invalid JSON 
          echo {{inputs.parameters.data1}}
          echo {{inputs.parameters.data2}}
          echo {{inputs.parameters}}
          sleep 1000

# workflow pod log 결과
{"a":"apple     lemon"}
{"b":"banana"}
[{"name":"data1","value":"{\"a\":\"apple     lemon\"}"},{"name":"data2","value":"{\"b\":\"banana\"}"}]
{a:apple lemon}
{b:banana}
[{name:data1,value:{"a":"apple lemon"}},{name:data2,value:{"b":"banana"}}]
{a:apple     lemon}
{b:banana}
[{name:data1,value:{"a":"apple     lemon"}},{name:data2,value:{"b":"banana"}}]

#####

# data3 의 값이 json 이고 필드값으로 yaml, xml 등이 escaped 로 된 복잡한 파라메터인 경우 다음과 같이 처리해도 올바른 JSON 으로 출력되지 않는다.
echo '{{inputs.parameters.data3}}' # /argo/staging/script 에러
echo {{inputs.parameters.data3}} # " 가 빠진 invalid JSON
echo -e {{inputs.parameters.data3}} # "가 빠지고 \n 등에서 /argo/staging/script 에러

# jq 가 포함된 이지미를 사용해 script 에 다음과 같이 환경변수를 파싱해서 사용했다.
echo "$ARGO_TEMPLATE" | jq -r '.inputs.parameters[] | select(.name=="data3").value' | jq -r .workflow_yaml > /tmp/data.yaml

kaniko args

# k8s pod 환경에서 이미지 빌드를 위해 kaniko 를 사용한다.
# github pull, docker registry push 를 위해 다음 2가지를 준비한다.
# github > personal_access_token > repo 접근 권한체크해서 생성
kubectl create secret generic ysoftman-generic-secret \
  --from-literal=git-personal-access-token="abc123" \
  --namespace=ysoftman-test

# 이미지 푸시를 위새 docker secret 생성
kubectl create secret docker-registry ysoftman-secret \
  --docker-server=ysoftman \
  --docker-username=ysoftman \
  --docker-password=ysoftman123 \
  --namespace=ysoftman-test

# 이제 argo workflow 로 kaniko(executor) 로 실행하는데,
# dockerfile ARGS 에 전달하기 위해 --build-arg 옵션을 아래와 같이 사용했다.
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: ysoftman-test
  namespace: ysoftman-test
spec:
  entrypoint: build-image-and-push
  serviceAccountName: workflow-template
  templates:
    - name: build-image-and-push
      inputs:
        parameters:
          - name: fruit
            value: "lemon"
      script:
        image: "rockylinux:latest"
        command: [bash]
        source: |
          curl -X GET "https://httpbin.org/get" -H "accept: application/json"
          echo "-----"
          echo $ysoftman1
          echo $ysoftman2
        env:
          - name: ysoftman1
            value: lemon
          - name: ysoftman2
            valueFrom:
              secretKeyRef:
                name: my-secret # name of an existing k8s secret
                key: mypassword # 'key' subcomponent of the secret
      container:
        name: kaniko
        image: "gcr.io/kaniko-project/executor:debug"
        env:
          - name: github_personal_access_token
            valueFrom:
              secretKeyRef:
                name: ysoftman-generic-secret
                key: git-personal-access-token
        command: [executor]
        args:
          - "--context=git://$(github_personal_access_token)@github.com/ysoftman/foobar.git#refs/heads/branch1"
          - "--context-sub-path=./aaa/bbb"
          - "--dockerfile=Dockerfile"
          - "--destination=ysoftman/foobar:test"
          - "--build-arg var1={{inputs.parameters.fruit}}"
        volumeMounts:
          - name: kaniko-secret
            mountPath: /kaniko/.docker/
      volumes:
        - name: kaniko-secret
          secret:
            secretname: ysoftman-secret
            items:
              - key: .dockerconfigjson

# 그런데 pod 로그에 다음과 같이 에러가 발생한다.
Error: unknown flag: --build-arg var1

# --build-arg 사용시 IFS(Internal Field Separator) 공백구분을 지원하지 않아 export IFS='' 를 설정하라고 한다.
# 위 와 같은 yaml 에서는 IFS 설정이 안되니 다음과 같이 구분하면 된다.
args:
  - "--build-arg"
  - "var1={{inputs.parameters.fruit}}"

# 그리고 container > args 에서 env 참조시 $(VAR_NAME) 를 사용해야 한다.
args:
 - "foobar=$(github_personal_access_token)"

argo-workflows usage

# ingress 가 없는 경우 k8s api 와 통신하는 프록시 서버 띄우기
kubectl proxy

# 프록시 서버로 접근
http://localhost:8001/api/v1/namespaces/{네임스페이스}/services/{서비스}/proxy

# argo-workflows 로그인
# 다음 명령으로 토큰값을 파악해서 입력한다.
echo -n "Bearer $(kubectl get secret argo-workflows -n argo-workflows -o=jsonpath='{.data.token}' | base64 --decode)" | pbcopy

#####

# argo cli 사용하기
# 로그인 설정
# SSO 로그인만 api 가 허용되는 경우가 있어 아래 kubect 으로 secret 토큰으로 권한없음이 나오면
# http://argo-workflow.ysoftman.test/userinfo 에서 복사해오면 토큰값도 있다.
export ARGO_SERVER='argo-workflow.ysoftman.test:80'
export ARGO_HTTP1=true
export ARGO_SECURE=false
export ARGO_BASE_HREF=""
export ARGO_TOKEN="Bearer $(kubectl get secret argo-workflows -n argo-workflows -o=jsonpath='{.data.token}' | base64 --decode)"
export ARGO_NAMESPACE="argo-workflows"

# argo_workflow_hello_world.yaml 작성
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: hello-world
spec:
  entrypoint: whalesay
  templates:
    - name: whalesay
      container:
        image: docker/whalesay
        command: [cowsay]
        args: ["hello world"]
        resources:
          limits:
            memory: 32Mi
            cpu: 100m

# workflow 작업 수행(argo server 에서도 워크플로우가 생성된것을 확인할 수 있다.)
argo submit ./argo_workflow_hello_world.yaml

# workflow lis 로 workflow 이름 확인
argo list

# workflow log
argo logs hello-worldd9z5q --follow

# delete workflow
argo delete hello-worldd9z5q

Prev