태그 보관물: container

Rust 프로그래밍에서 map() 활용

Python이 그러하듯 Rust도 declarative programming(선언형 프로그래밍)을 지원한다. 그 중 map()은 이러한 코딩스타일의 대표처럼 사용되고는 하는데, 이것을 이용하면 길고 장황한 코드를 간결하게 나타낼 수 있다.

기본적 사용법

1 부터 10까지 1씩 증가하는 값을 가진 i32 형의 10칸짜리 배열 arr이 있다고 할 때, 그 안에 있는 각 원소들에 2를 곱하는 코드이다.

// 배열의 각 요소들에 x2를 수행하고 결과를 출력.
// 실행결과:
// Doubled array: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

fn main() {
    // i32 type 배열 선언.
    let arr: [i32; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // map()을 사용해서 각 element들에 x2를 수행.
    let d_arr = arr.map(|x| x * 2);
    println!("Doubled array: {:?}", d_arr);
}

위의 코드는 Python과 같은 다른 언어에서도 사용하는 형식을 가지고 있어서 직관적이고 그 내용을 이해하기도 비교적 쉽다. 그렇다면 vector에 대해 같은 코드를 작성하면 어떨까? 10칸짜리 vector, vec를 만들고 동일한 동작을 수행해 보도록 하자.

// 벡터의 각 요소들에 x2를 수행하고 결괄르 출력.
// 실행결과:
// Doubled vector: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

fn main() {
    // Vector를 선언하고 1 부터 10까지 값들로 초기화.
    let vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // map()을 사용해서 각 element들에 x2를 수행.
    let d_vec = vec.iter().map(|x| x * 2).collect::<Vec<_>>();
    println!("Doubled vector: {:?}", d_vec);
}

앞에서의 배열에 대한 map() 연산과 달리 뭔가 복잡해 졌다. 10번째 줄을 보면, map()을 호출하기 전에 iter()를 통해서 먼저 iterator(반복자)를 받고, 그 다음에 map()의 내용을 lambda로 수행한 다음에 collect::<Vec<_>>()라는 기괴한 구문으로 끝을 맺고 있다.

The “Lazy Pipeline”

이와 같이 data soruce로 부터 반복자를 받아서 map()을 통해 형태를 변경하고 다시 collect()로 넘겨 최종 처리하는 패턴(Create – Transform – Consume)이 Rust에서 자주 사용되는데 이것을 “Iterator Bridge” 패턴 혹은 “Lazy Pipeline”이라고 부른다.

이렇게 복잡한 pipeline을 거치는 이유는 크게 두가지 정도를 생각해 볼 수 있는데, 첫번째는 원하는 목적에 따라 각 단계의 함수(메소드)들을 다른 종류로 대체할 수 있기 때문이고 두번째는 앞서 살펴본 array와 달리 컴파일 시간에 그 크기가 정해지지 않는 동적 타입에 대해 실행시간을 최대한 미루는 게으른 처리(lazy evaluation)을 이용하여 성능을 향상시키기 위함이다.

각 함수(메소드)들의 대체

위의 예제에서는 Vector type의 source에 대하여 x2라는 transformation을 수행하고, collect::<Vec<_>>()로 consume해서 새로운 vector를 생성해 냈다. 반복자를 호출하는 방법에도 소유권을 넘겨주는 지 혹은 빌려오는지에 따라 iter_into() 혹은 iter()와 같은 다양한 종류의 반복자 반환 메소드를 사용할 수 있고, transformation을 수행하는 메소드도 map() 뿐만 아니라 특정한 조건을 만족하는 원소를 걸러내는 filter()같은 메소드들을 사용할 수 있으며, 최종단의 consumer 역시도 새로운 container를 만들어 내는 collect()외에도 요소의 갯수를 반환하는 count() 나 요소의 합을 계산하는 sum() 같은 다양한 메소드들로 목적에 맞게 파이프라인을 구성할 수 있다.

동일한 패턴을 유지하면서 vector내부에 있는 짝수의 총 합을 계산하는 다음 코드를 한번 보자.

// 벡터안의 짝수만 걸러서(filter) 합을 구하기
// 실행결과:
// Sum of all even numbers: 30

fn main() {
    let vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let sum = vec.iter().filter(|x| *x % 2 == 0).sum::<i32>();
    println!("Sum of all even numbers: {}", sum);
}

게으른 처리 (Lazy Evaluation)

Adapter인 map() 혹은 filter()로 호출한 구문은 consumer(collect(), fold(), sum(), count(), for loop 등)을 만나기 전까지는 아무런 수행도 하지 않았다가 consumer를 만나고 나서야 동작을 수행하게 된다. 이러한 동작은 pipeline이 구성된 후에 수행되기 때문에 불필요하게 중간상태를 보관하기 위한 메모리를 할당할 필요 없이 최적화된 코드 수행을 할 수 있도록 해준다. 실행시간에 크기가 변화하는 Vector와 같은 container를 감안할 때, 코드 수행단계마다 처리해서 임시 메모리에 쌓아두는 것 보다, 필요 할 때까지 미루어 뒀다가 최종 상태를 감안해서 처리하는 게으른 처리는 메모리 소모와 성능면에서 좋은 전략이 될 수 있다.

Option과 Result에 대한 map() 사용

언뜻 보면 일관성이 없어 보이기도 하지만, Option과 Result에도 map()을 사용할 수 있는데, 이는 Option의 Some()이나 Result의 Ok() 상태 처럼 정상동작한 경우에 대해 수행할 동작을 정의하고자 할 때 사용할 수 있어서, if let 구문이나 match 구문을 대체하여 코드를 간결하게 유지할 수 있도록 해준다.

// 실행결과:
//the value is SOME: 'some value'
//the result is OK: Result was good

use std::io::{Error, ErrorKind};

fn main() {
    // Option, map()
    let opt = Some("some value");
    //let opt: Option<String> = None;
    opt.map(|x| println!("{}", format!("the value is SOME: '{x}'")));

    // Result, map()
    let res: Result<String, Error> = Ok("Result was good".to_string());
    //let res: Result<String, Error> = Err(Error::new(ErrorKind::Other, "Some error"));
    let _ = res.map(|x| println!("{}", format!("the result is OK: {x}")));
}

Conclusion

Rust에서 사용되는 map()의 활용에 대해 살펴 보았다. Container에 대해 map()을 사용하는 것은 비교적 우리에게 익숙하지만, Option / Result에 map()을 사용하는 개념은 상대적으로 그렇지 않은데, 많은 설명들에 따르면, 이것은 “상자 속에 들어 있는” 어떤 값에 대해 그 내부를 직접 들여다 보지 않고 적용한다는 점에서 container와 Option, Result 공통으로 적용할 수 있는 선언형 프로그래밍의 철학이 적용된 것이라고 한다.

Multi-container app의 Bad Gateway(502) 문제 디버깅

Intel Training and Learning Suite(TLS)는 처음으로 경험해보는 multi-container application이다. 중간에 native개발을 위한 환경 설정을 끼워 넣어 보려고 이것 저것 시도하는 중에 web interface를 담당하는 tls_proxy라는 docker가 Bad Gateway를 띄우면서 문제가 생겼다. 디버깅 하는 과정에서 사용해 본 docker-compose명령어 들의 쓰임새가 유용할 것 같아서 기록으로 남겨 둔다.

Port number와 docker찾기: docker-compose ps

Web browser로 서비스에 접근하면(https://localhost) Bad Gateway(502) error가 발생하는데 처음으로 web-browser의 접속 요청을 받아서 처리하는 docker container는 docker-compose ps 명령어로 docker image별로 listening하는 port number들을 찾을 수 있다.

TLS의 경우는 tls_proxy가 http요청 처리를 위한 80번과 https를 위한 443번 port를 listening하고 있다.

$ docker-compose ps
    Name                  Command               State     Ports                     
------------------------------------------------------------------
tls_apiui      ./webservices/start.sh           Exit 0                                                                                                                                                     
tls_core       ./start.sh                       Up                                                                                                                                                         
tls_mongo      docker-entrypoint.sh --tls ...   Up       127.0.0.1:27017->27017/tcp                                                                                                                        
tls_openvino   /bin/bash                        Exit 0                                                                                                                                                     
tls_proxy      /docker-entrypoint.sh /bin ...   Up       0.0.0.0:443->443/tcp,:::443->443/tcp, 80/tcp                                                                                                      
tls_rabbitmq   docker-entrypoint.sh /init ...   Up       0.0.0.0:15672->15672/tcp,:::15672->15672/tcp, 15691/tcp, 15692/tcp, 0.0.0.0:1883->1883/tcp,:::1883->1883/tcp, 25672/tcp, 4369/tcp, 5671/tcp, 5672/tcp, 0.0.0.0:8883->8883/tcp,:::8883->8883/tcp
tls_redis      docker-entrypoint.sh sh st ...   Up       127.0.0.1:6379->6379/tcp

Docker의 log 보기: docker logs tls_proxy

tls_proxy가 요청을 처리하지 못한 이유는 docker logs <서버 이름>으로 확인할 수 있다.

$ docker logs tls_proxy
2023/05/31 01:31:06 [error] 12#12: *5 connect() failed (113: Host is unreachable) while connecting to upstream, client: 172.31.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://172.31.0.8:3000/", host: "localhost"
...

사설 네트워크인 172.31.0.x로 docker service들을 구성했는데, 그중 인터페이스를 담당하는 172.31.0.1 docker가 172.31.0.8에 요청을 연결해 주어야 하는데 이 부분에서 오류가 나고 있는 모양이다.

Docker network 확인: docker network ls

현재 docker용으로 구성되어 있는 network을 확인해 보면 다음과 같다.

$ docker network ls
NETWORK ID     NAME                DRIVER    SCOPE
d15b28e7a1d5   bridge              bridge    local
acc99b5947d1   cvat_cvat           bridge    local
4485972f8274   host                host      local
6f21c18ba66f   none                null      local
20d132ccfc2a   tls_default         bridge    local

각 네트워크의 구성을 검사(inspect)해 볼 수 있는데 docker network inspect cvat_cvat명령어로 해당 네트워크를 검사해 보면 다음과 같이 위에서 오류가 발생했던 ip인 172.21.0.8번(tls_openvino)이 등록되지 않은 것을 볼 수 있다.

$ docker network inspect cvat_cvat|grep "Name\|IPv4Address"
        "Name": "cvat_cvat",
                "Name": "cvat_redis",
                "IPv4Address": "172.31.0.4/16",
                "Name": "cvat_opa",
                "IPv4Address": "172.31.0.3/16",
                "Name": "cvat_db",
                "IPv4Address": "172.31.0.5/16",
                "Name": "traefik",
                "IPv4Address": "172.31.0.2/16",
                "Name": "cvat",
                "IPv4Address": "172.31.0.6/16",
                "Name": "cvat_ui",
                "IPv4Address": "172.31.0.7/16",
                "Name": "tls_proxy",
                "IPv4Address": "172.31.0.9/16",

Docker의 log 보기: docker logs tls_openvino

문제가 되고 있는 docker container를 특정했으니 docker logs tls_openvino명령어로 다시 한번 해당 container의 로그를 확인해 본다.

error: A hook (`userconfig`) failed to load!
error: Failed to lift app: Error: Attempted to `require('/home/tls/webservices/apiserver/config/env/production.js')`, but an error occurred:
--
Error: ENOENT: no such file or directory, open '../../thirdparty/security/ca/ca_certificate.pem'
    at Object.openSync (fs.js:498:3)
    at Object.readFileSync (fs.js:394:35)
    at Object.<anonymous> (/home/tls/webservices/apiserver/config/env/production.js:70:15)

../../third party/security/ca/ca_certificate.pem파일을 읽지 못해서 오류가 발생했다고 나오는데, 실제로 container에서는 해당 파일의 위치를 상대 경로로 접근하지 않는다. Call stack에 나와 있는 production.js line 70근처의 내용을 보면 먼저 절대경로인 /run/secrets/*에 접근을 시도할 때 예외가 발생해서 상대 경로로 접근을 시도하고(개발용 코드로 추정) 이 마저도 실패한 것이 로그에 나온 것이다.

apiserver/config/env/production.js

try {
  tls_ca = fs.readFileSync("/run/secrets/ca_tls")
  tls_server_cert = fs.readFileSync("/run/secrets/tlsserver_cert")
  tls_server_key = fs.readFileSync("/run/secrets/tlsserver_key")
} catch (err) {
  tls_ca = fs.readFileSync("../../thirdparty/security/ca/ca_certificate.pem");
  tls_server_cert = fs.readFileSync("../../thirdparty/security/TLS_server_cert.crt");
  tls_server_key = fs.readFileSync("../../thirdparty/security/TLS_server_key.pem");
}

정리하자면, 문제는 server secret을 생성하는 과정에서 발생한 것이 docker image상의 key file path read에서 오류를 발생시키고 tls_openvino container가 제대로 뜨지 못한 문제이다.

해결(?)

문제의 원인은 생각보다 싱거웠는데, docker-compose 명령어를 실행시키는 native용 script에서 다음 줄을 연결하기 위한 \를 빼먹는 사소한(!) Bash 문법 오류가 발생 했었고, 이 오류가 무시된 채로 docker-compose build 명령어가 계속 수행되어 service가 up되는 상황까지 된 것이었다.

-       && pip install -U pip wheel setuptools
+       && pip install -U pip wheel setuptools \
        && pip install \

위의 수정과 함께 오류가 생기면 build를 멈추도록 set -e를 bash script에 추가 하는 것으로 이 문제는 일단락 되었다.