$ sudo service docker start  
Redirecting to /bin/systemctl start docker.service  
Job for docker.service failed because the control process exited with error code. See "systemctl status docker.service" and "journalctl -xe" for details.

실행하려니 실패.

$ systemctl status docker.service  
● docker.service - Docker Application Container Engine  
   Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)  
   Active: failed (Result: exit-code) since 화 2022-04-05 12:17:01 KST; 12s ago  
     Docs: [https://docs.docker.com](https://docs.docker.com)  
  Process: 25074 ExecStart=/usr/bin/dockerd (code=exited, status=1/FAILURE)  
 Main PID: 25074 (code=exited, status=1/FAILURE)

상세 사유 없어서 확인이 안됨.

이럴땐 도커 데몬을 직접 실행해본다.

$ sudo dockerd --debug  
Error starting daemon: Error initializing network controller: Error creating default "bridge" network: failed to allocate gateway (10.252.0.0): Address already in use

나의 경우 bridge ip 설정 떄문에 실패한 것.

'개발 > 기타' 카테고리의 다른 글

[SpringSecurity] Https same origin 에 대한 CORS 설정  (0) 2020.06.09
URI와 URL의 개념, 차이  (2) 2020.05.23
SAML / OAuth2.0 / OpenIDConnect  (1) 2020.05.12
[PHP] Call to undefined function dl()  (0) 2020.02.10
카프카(Kafka)  (0) 2020.01.30

기존 스프링 Security에서 same-origin의 경우 별도 CORS설정을 안해줘도 문제가 없었지만

origin이 Https가 되는 경우엔 cors 설정을 해줘야 한다. 

 

이렇게 Origin 헤더가 https인 경우 CORS 설정이 필요하다. 

Origin: https://test.io

 

또한 allow method 디폴트가 GET,HEAD, POST 이므로

PUT과 같은 method를 사용하는 경우

  public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedMethods("*")
            .allowedOrigins("https://test.io")
            .allowedHeaders("*")
            .allowCredentials(true);
      }
    };
  }

registry.allowedMethods("*") 설정이 필요하다. 

 

'개발 > 기타' 카테고리의 다른 글

Docker service 실행 안될 때  (0) 2022.04.05
URI와 URL의 개념, 차이  (2) 2020.05.23
SAML / OAuth2.0 / OpenIDConnect  (1) 2020.05.12
[PHP] Call to undefined function dl()  (0) 2020.02.10
카프카(Kafka)  (0) 2020.01.30

'URI가 URL을 포함한다' 정도만 알고 있었는데, 제대로 정리해보고자 한다.

URI, URL, URN의 개념

모두 리소스 식별자이다.
리소스를 구분하고 싶은 것이다.

 

https://danielmiessler.com/study/difference-between-uri-url/

 

예를 들어 어떤 두 사람이 있다고 치자.
두 사람을 구분하고 싶다.
어떻게 구분할까?

  1. 이름을 붙인다. --> URN
    첫번째 사람 - 홍길동,
    두번째 사람 - 박길동

  2. 그 리소스를 얻을 수 있는 주소(위치)를 표현한다. --> URL
    첫번째 사람 - 서울시 아무구 아무동 123-1
    두번째 사람 - 서울시 아무구 아무동 456-1

이러한 방법으로 리소스를 명확히 식별할 수 있으면(구분할 수 있으면) 그건 URI다.
따라서 URN(Uniform Resource Name) 과 URL(Uniform Resource Locator) 모두 URI(Uniform Resource Identifier)다.

여기서 하나씩 짚고 넘어가보자.

URN이란?

리소스에 고유한 이름을 부여해서 구분하겠다는 방식이다.
여기서 중요한건 고유해야한다는 것이다.

예를 들어 urn:isbn:0-486-27557-4 를 검색하면 로미오와 줄리엣이 나온다.
ISBN은 국제표준도서번호이고, 국제표준으로 도서에 고유한 이름 을 부여한 것이다.
이건 서로 약속하는 것이다. 중복이 없어야 한다.

그런데 여기서 URL과 차이가 있다.
URN은 리소스를 식별만 할뿐 리소스를 얻을 수 있는 위치는 포함하지 않는다
리소스 위치에 상관 없이 고유하게 식별하려는 목적이다.

URL이란?

리소스를 식별하는 방법 중에, 리소스의 위치로 식별하는 방법이다.

ex) https://tools.ietf.org/html/rfc1738 --> 이 URL로 특정 RFC문서(리소스)를 얻을 수 있다.
ex) https://somewhere.com/user?userId=123 --> 이 URL로 특정 유저 정보(리소스)를 얻을 수 있다.

리소스를 위치 로 식별하겠다는 것이다.

URN과 비교를 해보자.

https://book.com/romio-juliet
https://book.net/romio-juliet

둘 다 동일한 로미오와 줄리엣 리소스를 가리킨다고 했을 때 URL은 각각을 서로 다른 리소스로 식별한다.

중간 정리

리소스 식별자 --> URI = URN + URI + α
이름으로 식별 --> URN
위치로 식별 --> URL

혼란

관련해서 구글링을 해보면 URL의 정의에 대한 여러 의견이 있어 혼란스러운 부분이 있었고, 이를 나름대로 정리해보고자 한다.

어디까지 URL인가

아래와 같이 정리된 글이 있었다.

https://somewhere/search?q=uri 아래와같은 주소가 있다고하자. query string인 q의 값에 따라 여러가지 결과값을 가져올 수 있다. 위 주소에서 URL은 https://somewhere/search까지이고, 내가 원하는 정보를 얻기위해서는 q=uri라는 식별자가 필요하므로, https://somewhere/search?q=uri 이 주소는 URI이지만 URL은 아니다.

아니다. 쿼리스트링까지 있어야 리소스를 명확히 식별할 수 있으므로 https://somewhere/search?q=uri이 URL이고 URI다. 쿼리스트링도 URL에 포함된다.

또 다른 글이 있었다.

http://somewhere.com/132 에서 http://somewhere.com까지만 URL이다.

아니다. 정확히 132에 해당하는 리소스를 식별해야하므로 http://somewhere.com/132 까지가 URL이다.
물론 http://somewhere.com에 해당하는 리소스가 있으면 http://somewhere.com 자체도 URL이지만 132에 대한 URL은 http://somewhere.com/132인 것이다.

이는 URL의 범위가 어디까지 인가에 대한 혼란인 것 같다.
RFC1737에 따르면 PATH, 쿼리스트링 모두 URL에 포함된다.

  httpaddress     http://hostport[/path][?search]   

이런 혼란이 생긴 이유중에 하나로 HttpServletRequest의 잘못도 있다고 생각한다.

    @GetMapping("/user")
    public void test(HttpServletRequest request, @RequestParam("id") int id) {
        String uri = request.getRequestURI();
        String url = request.getRequestURL().toString();
        logger.info("uri - {}, url - {}", uri, url);
    }

로그를 찍어보면 아래와 같이 나온다.
uri - /user, url - http://localhost:8080/user

이걸보고, '아 url에 쿼리스트링은 포함되지 않는구나' 에 더불어 'uri는 path 부분을 말하는구나' 라고 착각할 수 있을 것 같다.

HttpServletRequest의 문서를 읽어보면

getRequestURI
public java.lang.String getRequestURI()
Returns the part of this request's URL from the protocol name up to the query string in the first line of the HTTP request. The web container does not decode this String. For example:
First line of HTTP request Returned Value
POST /some/path.html HTTP/1.1 /some/path.html
GET http://foo.bar/a.html HTTP/1.0 /a.html
HEAD /xyz?a=b HTTP/1.1 /xyz
To reconstruct an URL with a scheme and host, use HttpUtils.getRequestURL(javax.servlet.http.HttpServletRequest).

Returns:
a String containing the part of the URL from the protocol name up to the query string
See Also:
HttpUtils.getRequestURL(javax.servlet.http.HttpServletRequest)

getRequestURI()는 URL(URL은 URI이므로)의 일부분(path)만 리턴한다고 되어있고, 전체 URL을 얻고 싶으면 다른 방법을 쓰라고 되어있다.
getRequestURI()의 리턴값 /user 은 URL의 일부일 뿐, URL은 아니다. method명이 혼란을 주는 듯 하다.

getRequestURL
public java.lang.StringBuffer getRequestURL()
Reconstructs the URL the client used to make the request. The returned URL contains a protocol, server name, port number, and server path, but it does not include query string parameters.
Because this method returns a StringBuffer, not a string, you can modify the URL easily, for example, to append query parameters.

This method is useful for creating redirect messages and for reporting errors.

Returns:
a StringBuffer object containing the reconstructed URL

여기서는 또 URL에서 쿼리스트링을 빼고 리턴하겠다고 한다.
때문에 쿼리스트링은 URL이 아닌 것 처럼 혼란을 주는 것 같다.
getRequestURL()의 리턴값 http://localhost:8080/user은 URL의 일부일 뿐, URL은 아니다.

결론

  • URI는 리소스 식별자이다.
  • URL은 리소스를 어디서 얻을 수 있는 지 위치를 말해준다. URL는 식별방법의 일부이다.
  • 또 다른 식별방법으론 고유한 이름을 부여하는 URN이 있다.

Appendix

RFC says

A URI can be further classified as a locator, a name, or both. The term “Uniform Resource Locator” (URL) refers to the subset of URIs that, in addition to identifying a resource, provide a means of locating the resource by describing its primary access mechanism (e.g., its network “location”).
rfc 3986, section 1.1.3

URI는 위치, 이름 등이 될 수 있다.

The URI itself only provides identification; access to the resource is neither guaranteed nor implied by the presence of a URI.
rfc 3986, section 1.2.2

URI는 식별만 할 뿐, 리소스를 어디서 얻을 수 있는지를 반드시 제공해야하는 것은 아니다.

Each URI begins with a scheme name, as defined in Section 3.1, that refers to a specification for assigning identifiers within that scheme.
rfc 3986, section 1.1.1

참고자료

https://danielmiessler.com/study/difference-between-uri-url/
https://tools.ietf.org/html/rfc1738

'개발 > 기타' 카테고리의 다른 글

Docker service 실행 안될 때  (0) 2022.04.05
[SpringSecurity] Https same origin 에 대한 CORS 설정  (0) 2020.06.09
SAML / OAuth2.0 / OpenIDConnect  (1) 2020.05.12
[PHP] Call to undefined function dl()  (0) 2020.02.10
카프카(Kafka)  (0) 2020.01.30

세가지 인증/인가 표준에 대해 공부하면서 나름대로 이해한 내용을 정리해보았다.
퍼즐 조각 맞추듯 이해한 내용이기 때문에 정확하지 않을 수 있다.

SSO(Single Sign On)

SSO가 없는 상황
SSO가 도입된 상황

  • 한번의 로그인으로 여러 어플리케이션 이용한다. (로그인 피로도 감소)
  • 인증을 못하면 전체 어플리케이션을 이용할 수 없다.
  • 비인가 유저가 일단 권한을 얻으면 모든 어플리케이션을 이용할 수 있게 된다.

한번만 로그인하면 된다?

정확히는 브라우저당 로그인을 한번만 하면 된다.

예를 들어 a.com, b.com이 같은 idp(myidp.com)를 바라보고 SSO가 되어있다고 할때,

  1. 인증안한 상태로 a.com 접속
  2. myidp.com으로 리다이렉트
  3. myidp.com 세션 쿠키가 없으므로 사용자에게 로그인 요청
  4. 인증 성공하면 myidp.com에 세션쿠키를 생성
  5. a.com의 콜백 url로 리다이렉트, a.com에 세션쿠키 생성
  6. 최초 요청 페이지로 다시 리다이렉트

이 상태에서 b.com 접속한다고 해보자. 다시 로그인해야할까? 아니다. 왜 그런걸까?

  1. b.com에는 세션 쿠키가 없는 상태로 접속
  2. myidp.com으로 리다이렉트
  3. myidp.com 세션쿠키가 있으므로 (위의 3에서) 사용자 로그인 필요없이 인증성공 내려줌
  4. b.com의 콜백 url로 리다이렉트, b.com에 세션쿠키 생성
  5. 최초 요청 페이지로 다시 리다이렉트

때문에 브라우저에 myidp.com의 세션쿠키만 남아있다면 다른 사이트에서 별도의 인증이 필요 없는 것이다.

SAML2.0 / OAUTH2.0 / OIDC(OpenIDConnect)

출처: https://spin.atomicobject.com/2016/05/30/openid-oauth-saml/

OAuth는 인증보단 인가 목적의 표준이므로 SAML과 OIDC를 비교해보자.
SSO과 같이 통합인증(federated authentication)을 위해 SAML이 있었다.
그런데 단점이 있다. SAML은 클라이언트가 웹사이트인 경우에는 적합하지만 모바일 어플리케이션 등에는 적합하지 않다. (그 이유는 아래에서)
이런 단점들을 OAuth 프로토콜을 이용해 보완하게 OIDC이다.

SAML(Security Assertion Markup Language)과 OIDC

SAML 인증 플로우

SAML은 SP가 웹 사이트여야만 한다. OIDP는 웹이나 모바일앱 등이 가능하다.

SAML은 백채널을 (거의) 안쓴다. 브라우저를 통해서만 SP와 IDP가 인증정보를 주고 받는다.
서버간 통신없이 클라이언트를 통해서 인증 데이터를 주고 받기 때문에, POST 바인딩을 통해 인증정보 + 리다이렉션을 처리한다. ( POST Binding) 헤더를 사용하는게 아니라 HTML내에 스크립트를 심어서 리다이렉트를 시키기 때문에 클라이언트가 웹사이트여야 한다. 모바일웹에서 SAML 방식을 사용한다고 하면 별도로 브라우저를 띄워서 구현해야 할 텐데 그럼 사용자 경험이 떨어질 것이다.

IDP의 응답으로 내려온 HTML에서 스크립트로 리다이렉트 처리하는 예시이다.
form으로 POST 요청을 만들어 IDP에서 얻은 인증 정보를 SP에게 전달한다.
스크립트가 실행되기 위해선 브라우저라는 환경이 필요하다.

<form method=’post’ action=’https://localhost:9443/samlsso'>
  <input type=’hidden’ name=’SAMLRequest’ value=’PD94bWwgdmVyc2lvbj0iMS4wln…….’>
  <input type=’hidden’ name=’RelayState’ value=”trtreey34234fdfes’’>
  <button type=’submit’>POST</button>
</form>

때문에 이런 단점을 보완한 서버간 통신을하는 OAuth 프로토콜 기반 OIDC가 나오게 된 듯하다.

SAML의 XML Assertion이 OIDC의 ID token에 대응되는 개념이다.

아래가 사용자 정보를 담은 SAML의 XML Assertion이다.

```
<samlp:ArtifactResponse
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="identifier_5"
InResponseTo="identifier_4"
Version="2.0"
IssueInstant="2004-12-05T09:22:05Z">
<!-- an ArtifactResponse message SHOULD be signed -->
<ds:Signature
  xmlns:ds="http://www.w3.org/2000/09/xmldsig#">...</ds:Signature>
<samlp:Status>
  <samlp:StatusCode
    Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<samlp:Response
  xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
  xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
  ID="identifier_6"
  InResponseTo="identifier_3"
  Version="2.0"
  IssueInstant="2004-12-05T09:22:05Z"
  Destination="https://sp.example.com/SAML2/SSO/Artifact">
  <saml:Issuer>https://idp.example.org/SAML2</saml:Issuer>
  <ds:Signature
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#">...</ds:Signature>
  <samlp:Status>
    <samlp:StatusCode
      Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="identifier_7"
    Version="2.0"
    IssueInstant="2004-12-05T09:22:05Z">
    <saml:Issuer>https://idp.example.org/SAML2</saml:Issuer>
    <!-- a Subject element is required -->
    <saml:Subject>
      <saml:NameID
        Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
        user@mail.example.org
      </saml:NameID>
      <saml:SubjectConfirmation
        Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData
          InResponseTo="identifier_3"
          Recipient="https://sp.example.com/SAML2/SSO/Artifact"
          NotOnOrAfter="2004-12-05T09:27:05Z"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions
      NotBefore="2004-12-05T09:17:05Z"
      NotOnOrAfter="2004-12-05T09:27:05Z">
      <saml:AudienceRestriction>
        <saml:Audience>https://sp.example.com/SAML2</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement
      AuthnInstant="2004-12-05T09:22:00Z"
      SessionIndex="identifier_7">
      <saml:AuthnContext>
        <saml:AuthnContextClassRef>
          urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
       </saml:AuthnContextClassRef>
      </saml:AuthnContext>
    </saml:AuthnStatement>
  </saml:Assertion>
</samlp:Response>
</samlp:ArtifactResponse>
```

다음은 OIDC의 id_token이다.
{ "iss": "https://accounts.google.com", "azp": "1234987819200.apps.googleusercontent.com", "aud": "1234987819200.apps.googleusercontent.com", "sub": "10769150350006150715113082367", "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q", "hd": "example.com", "email": "jsmith@example.com", "email_verified": "true", "iat": 1353601026, "exp": 1353604926, "nonce": "0394852-3190485-2490358" }

OIDC와 OAuth2.0의 차이

OIDC는 OAuth2.0 기반으로 돌아가기 때문에 비슷하나 목적이 다르다.

  • OAuth2.0은 3rd Party의 API 사용인가가 목적이고, OIDC는 통합인증(federated authentication), SSO가 목적이다.
    (OAuth를 인증 용도로도 사용할 수는 있다)

  • OAuth를 인증 용도로 사용할 수 있는데 왜 OIDC가 필요한걸까?

    • OAuth는 다양한 상황의 요구사항을 충족할 수 있또록 제네릭하게 구현되었다. 제네릭하다는 것은 그 만큼 클라이언트에서 할일이 많아진다. 반면 OIDC는 인증 초점이다. 상대적으로 클라이언트가 간편하게 사용할 수 있다. 또한 OAuth에서 제공하지 않는 부가적인 스펙(OP Discovery 등)도 있다. 때문에 인증만이 목적이라면 OIDC를 사용하는게 좋을 것 같다.
  • OIDC에는 id_token 개념이 추가된다. 사용자에 대한 부가적인 정가 id_token에 담긴다. 사용자 정보를 얻기 위해 별도 API콜을 하지 않아도 된다. (요청량이 많은 서비스에선 유의미한 효과일 것이다)

  • Oauth 인가 플로우

       +--------+                               +---------------+
       |        |--(A)- Authorization Request ->|   Resource    |
       |        |                               |     Owner     |
       |        |<-(B)-- Authorization Grant ---|               |
       |        |                               +---------------+
       |        |
       |        |                               +---------------+
       |        |--(C)-- Authorization Grant -->| Authorization |
       | Client |                               |     Server    |
       |        |<-(D)----- Access Token -------|               |
       |        |                               +---------------+
       |        |
       |        |                               +---------------+
       |        |--(E)----- Access Token ------>|    Resource   |
       |        |                               |     Server    |
       |        |<-(F)--- Protected Resource ---|               |
       +--------+                               +---------------+
  • OIDC 인증 플로우

OIDC가 OAuth2.0 기반이므로 플로우는 유사하다.
앞서 말했듯이 OIDC는 access_token외에 id token이라는 것을 추가로 받게 된다.

-   `ID token`  
    JWT 형태로, 누가 인증을 했는지, 그 사람에 대한 정보, 발행자(issuer)가 누구인지등의 정보가 담겨있다.

    ```
     {
       "iss": "https://accounts.google.com",
       "azp": "1234987819200.apps.googleusercontent.com",
       "aud": "1234987819200.apps.googleusercontent.com",
       "sub": "10769150350006150715113082367",
       "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
       "hd": "example.com",
       "email": "jsmith@example.com",
       "email_verified": "true",
       "iat": 1353601026,
       "exp": 1353604926,
       "nonce": "0394852-3190485-2490358"
     }
    ```

때문에 추가적인 API 콜없이 토큰을 열어서 유저정보를 획득할 수 있다.

정리

OIDC는 통합 인증을 위한 표준으로 SAML을

  • XML Assertion을 ID token (JWT)으로
  • 통신 플로우를 OAuth2.0 방식으로
    재작성했다고 볼 수 있다.

dl() 이란

dl은 PHP extension을 로드한다.
스크립트가 종료되면 언로드된다.


dl('php_sqlite.dll');

에러 원인

PHP 5.3.0 부터 dl이 제거되었다. (https://www.php.net/manual/en/function.dl.php)

 

여러 스크립트에서 A라는 extension을 사용한다고 할때 매 스크립트마다 로드와 언로드가 반복되므로 비효율이 발생하는 구조여서 제거된게 아닌게 싶다.

방안

php.ini 에서 extension을 로드해줘야 한다.

  1. php.ini 파일을 찾는다
    $ php --ini Loaded Configuration File: /etc/php.ini Scan for additional .ini files in: /etc/php.d Additional .ini files parsed: /etc/php.d/10-opcache.ini,

  2. php.ini 에 아래와 같이 추가
    extension=php_sqlite.dll

카프카의 특징

  • Pub/Sub 동작 방식이다.
    • Publisher와 Subscriber가 독립적으로 데이터를 생산하고 소비한다. 프로듀서와 컴포넌트를 디커플링하기 위한 좋은 수단이다.
    • 이런 느슨한 결합을 통해 둘 중 하나가 죽어도 서로간에 의존성이 없으므로 안정적으로 데이터를 처리할 수 있다.
      • 리시버와 컬렉터의 통신을 API로 한다고 가정해보자.
        • 컬렉터는 리시버가 요청한 것을 바로 처리해줘야 한다. (처리를 미룰 수 없다)
        • 실패가 났을 때 부분 재처리가 불가능하다.
        • 처리속도가 느린 컴포넌트 기준으로 처리량이 결정된다. 그러므로 트래픽이 몰리는 상황을 대비해 전체 파이프라인을 넉넉하게 산정해야 한다.
      • 중간에 카프카를 두게 되면 이런게 좋아진다.
        • 컬렉터는 리시버가 요청한 작업을 쌓아두고 나중에 처리할 수 있다.
        • 실패가 나도 컬렉터만 따로 재처리 할 수 있다.
        • 일시적으로 트래픽이 몰리는 상황을 대비해서 제일 프론트의 서버만 넉넉하게 준비해두면 된다. 물론 장기적이 되면 전체 파이프라인을 증설해야 겠지만.
    • 또한 설정 역시 간단해진다.
      • Publisher 따로, Consume 따로 독립적으로 설정할 수 있다.
  • 디스크 순차 저장 및 처리
    • 메시지를 메모리큐에 적재하면 데이터 손실 가능성이 있지만 카프카는 디스크에 쓰므로 데이터 손실 걱정이 없다.
    • 디스크에 쓰므로 상대적으로 속도가 느리지만, 순차처리 방식으로 디스크 I/O를 줄여 그렇게 느리지도 않다. 엄청 반응성이 높은 서비스가 아니라면 이 속도가 문제되진 않는다.

카프카 아키텍처 구성요소

  • 토픽
    • 카프카 안에는 여러 레코드 스트림이 있을 수 있고 각 스트림을 토픽이라고 부른다.
    • 하나의 토픽에 대해 여러 Subscriber가 붙을 수 있다.
  • 파티션
    • 각 토픽마다 데이터를 여러개의 파티션에 나누어서 저장/처리한다.
    • 토픽 사이즈가 커질 경우 파티션을 늘려서 스케일아웃을 할 수 있다.
    • 일반적으로 브로커 하나당 파티션 하나다.
    • 컨슈머 인스턴스 수는 파티션 갯수를 넘을 수 없다. 그러므로 병렬처리의 수준은 파티션 수에 의해 결정된다. 즉 파티션이 많을 수록 병렬처리 정도가 높아진다.
    • 각 파티션마다 Publish되는 레코드에 고유 오프셋을 부여한다. 때문에 레코드는 파티션 내에서는 유니크하게 식별된다. 하지만 파티션간에는 순서를 보장하지 않는다.
    • 전체 순서를 보장하고 싶으면 파티션을 하나만 두는 수 밖에 없는데, 이러면 데이터량이 많아져도 스케일아웃이 안되고, 컨슈머 인스턴스도 하나만 둘 수 있으므로 병렬 컨슈밍도 안된다.
  • 데이터 보관기간
    • 컨슘하고는 상관없다. 보관기간(Retention) 정책에 따른다.
    • 데이터 사이즈가 늘어날지라도 성능은 일정하게 유지된다.
  • 오프셋
    • 일반적으로 컨슈머가 오프셋을 순차적으로 증가하며 컨슘해하지만, 원한다면 컨슈머 마음대로 조정할 수 있다. 재처리가 필요한 경우 오프셋을 이전으로 돌릴 수도 있고 가장 최근 레코드 부터 처리할 수도 있다.
    • 컨슈밍 비용이 저렴하기 때문에 커맨드라인 컨슈머로 데이터를 "tail"하는 작업도 다른 컨슈머에 별 영향을 끼치지 않는다.
  • 프로듀서
    • 레코드를 프로듀스할 때 어느 토픽의 어느 파티션에 할당할 지를 결정한다.
    • 일반적으로 라운드로빈 혹은 원하는 대로 할당방식을 지정할 수 도 있다.
  • 컨슈머 그룹
    • 컨슈머 그룹 마다 독립적인 컨슘 오프셋을 가진다.
    • 컨슈머 그룹 내에서 처리해야할 파티션이 분배된다. 즉 하나의 파티션은 하나의 서버가 처리한다. 그룹에 서버가 추가되면 카프카 프로토콜에 의해 동적으로 파티션이 재분배 된다.
    • 하나의 토픽 레코드를 분산처리하는 구조라면 동일 컨슈머 그룹을 가지게 해야 한다.
    • 하나의 토픽 레코드에 각각 별도의 처리를 하는 다른 파이프라인이라면 서로 다른 컨슈머 그룹을 가지게 해야 한다.
  • 큐(Queue)와 Pub/Sub의 혼합 모델
    • 컨슈머그룹 내에서는 큐처럼 동작하고 컨슈머그룹간에는 Pub/Sub처럼 동작한다. 컨슈머그룹을 어떻게 두냐에 따라 두 방식 다 입맛에 맞게 선택할 수 있다.
  • 데이터 순서 보장
    • 일반적인 큐 모델에서 순차적으로 컨슘해간다고 해도 각 처리기에 도달하는 시간이 다르므로 순서가 보장되지 않는다.
    • 그렇다고 처리기를 1개만 두면 병렬성이 떨어진다.
    • 카프카는 토픽의 전체 순서는 보장하지 않지만 파티션별로는 처리 순서를 보장한다. (파티션은 하나의 컨슈머 인스턴스에서만 처리되므로). 즉 전체 순서 보장을 포기하고 부분 순서 보장을 취한 것이다. 대신 병렬성을 유지할 수 있다. 다르게 말하면 파티션당 컨슈머를 하나만 가져야하는 이유는 파티션 내에서 처리순서를 보장하기 위함이다.

데이터 복제

  • 각 파티션을 여러대의 서버에 복제해둔다. 이는 설정값을 따른다. 크래시리포트는 ?개의 리플케이션을 두었다.
  • 각 파티션 마다 특정 서버 한대를 리더로 선정하고 나머지 복제 파티션은 팔로워가 된다.
  • 리더가 모든 읽기/쓰기를 처리한다. 리더는 요청을 패시브하게 팔로워들에게 전파한다.
  • 각 서버는 하나의 리더 파티션을 갖는다. 때문에 트래픽은 각 서버로 밸런싱이 되는 셈이다.
  • 복제 수가 N개라면 N-1개까지 죽어도 실패복구가 가능하다.

주키퍼의 역할

  • 컨트롤러를 선출한다. 컨트롤러는 전체 파티션에 대한 리더/팔로워를 관리한다. 컨트롤러의 역할은 노드가 죽으면 다른 리플리카에게 리더가 되라고 명령을 내리는 역할을 한다.
  • 어떤 브로커가 살아있는지를 체크한다.
  • 어떤 토픽이 있고, 토픽에 파티션이 몇개 있고, 리플리카는 어디있고, 누가 리더가 될만한지, 각 토픽에 어떤 설정이 되어있는지를 관리한다.

오프셋 관리

  • 카프카에서 관리하는 오프셋은 두 종류가 있다.
    • Current Offset : 컨슈머에게 전송된 마지막 레코드의 포인터
    • Committed Offset : 컨슈머가 성공적으로 처리한 마지막 레코드의 포인터.
  • 오프셋 커밋 방법
    • 카프카에서 관리되는 방식 : _consumer_offsets 토픽에 오프셋이 저장된다.
      • 오토 커밋 : 일정 주기를 가지고 자동으로 커밋한다. 작업이 끝났든 안끝났든 주기만 되면 커밋한다.
        • 커밋되기 전에 리밸런싱이되면 중복처리가 발생한다.
        • 처리 완료되기 전에 커밋 후 프로세스가 죽으면 데이터 손실이 발생한다. (at-least-once 보장X)
      • 수동 커밋 : 수동으로 커밋 API를 호출한다. 정확히 작업이 끝나고 커밋하는 걸 보증하기 위해 사용한다.
        • 커밋되기 전에 리밸런싱되면 중복처리가 발생한다.
    • 자체적으로 관리하는 방식 : 별도 DB등에 자체적으로 오프셋을 관리한다.
  • 메시지 보증 전략
    • 일반적인 보증 전략은 다음과 같고 카프카는 at-least-once로 보증한다.
      • at-most-once : 중복X, 유실O
      • exactly-once : 중복X, 유실X, 구현하기 어렵다.(비용이 비싸다)
      • at-least-once : 중복O, 유실X
    • 데이터가 중복되는 경우는 다음과 같다. 어떤 데이터가 poll됐으나 commit되지 않은 시점에 리밸런싱 작업이 일어나면 이미 poll되어서 처리중인 데이터를 다른 컨슈머가 또 poll갈 수 있다.
    • 수동 커밋에서도 마찬가지다. 커밋 전에 리밸런싱이 되는 경우 중복으로 처리될 수 있다.
    • 가장 좋은건 exactly-once지만 비용이 비싸므로 적당한 타협지점은 at-least-once이다.중복되는 메시지는 메시지 ID나 시간등으로 어느정도 보정이 가능하기 때문이다.
    • 카프카 0.11 버전 부터 idempotent producer와 tracsaction producer의 등장으로 exactly-once를 보장할 수 있다고 한다.
      • 내부적으론 레코드의유니크ID, 트랜잭션ID, 프로듀서ID등을 조합해서 처리한다고 한다.
  • 크래시리포트의 오프셋 관리
    • 스파크 클러스터만 수동커밋을 하고 있다. 스파크는 보통 poll에서 프로세싱 완료까지 시간이 오래걸릴 것으로 예상되므로 오토커밋을 할 경우 데이터 손실 가능성이 커서 수동으로 커밋하는 걸로 보인다.
    • 오프셋을 카프카가 아닌 별도의 DB에 저장했다. commitAsync()에서 fail이 발생했던 거 같은데 정확한 원인은 모르겠다.

다른 미들웨어와 비교해보자.

  • RabbitMQ
    • 장점
      • 다양한 기능, 높은 성숙도
      • 20k/sec 처리 보장
  • Kafka
    • 장점
      • 고성능 고가용성
      • 분산처리에 효과적으로 설계됨
      • 100k/sec 처리 보장

겪었던 이슈들

ERROR [Thread-20] [Consumer clientId=consumer-13, groupId=xxx-collector] Offset commit failed on partition zxx.topic-1 at offset 199: The request timed out. 

원인은 글쎄.... 뭐였지...

하이퍼링크를 통해서 파일을 다운로드할 때 파일이름을 바꾸고 싶으면

<a href="/test.png" donwload="newname.png"></a>

으로 하면 newname.png으로 다운로드가 되지만,

<a href="http//blahblah.com/file/test.png" donwload="newname.png"></a>

cross-origin 요청에 대해서는 더 이상 download attribute를 지원하지 않는다.

 

결국 request에 파일명을 쿼리스트링으로 전달한다음 nginx 자체에서 처리하도록 바꿨다.

location /file {
  autoindex      on;
  alias /data01/file/;
  # 이 설정을 추가해준다. 
  add_header Content-Disposition 'attachment;filename=$arg_filename'; 
}
<a href="http//blahblah.com/file/test.png?filename="newname.png"></a>

'개발 > 기타' 카테고리의 다른 글

[PHP] Call to undefined function dl()  (0) 2020.02.10
카프카(Kafka)  (0) 2020.01.30
Vue.js에서 Props가 안넘어가는 문제 (undefined)  (0) 2020.01.08
[Java] 원격 디버깅(Remote debugging)  (0) 2019.12.18
[Nginx] 'Server' 헤더 제거  (0) 2019.02.07
<report-list :reports="reports" :fields="fields" :clickHandler="moveToReport"></report-list>

var ReportList = {
	Props: ['clickHandler']
};

new Vue({
  .. 
  methods:  {
    moveToReport: function() {
       ....
    }
  }
});

 

clickHandler를 넘기고 싶은데, report-list에서 props 를 찍어보면 계속 undefined로 떠서 도대체 왜 이러나 싶었다.

<report-list :reports="reports" :fields="fields" :handler="moveToReport"></report-list>

var ReportList = {
	Props: ['handler']
};

new Vue({
  .. 
  methods:  {
    moveToReport: function() {
       ....
    }
  }
});

대소문자를 섞어쓰지 않고 소문자로만 props명을 만드니 잘 작동했다.

 

관련해서 가이드가 있었다.

https://vuejs.org/v2/guide/components-props.html#Prop-Casing-camelCase-vs-kebab-case

 

Props — Vue.js

Vue.js - The Progressive JavaScript Framework

vuejs.org

HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you’re using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents:

DOM 템플릿에서는 camelCase->kebab-case로 변환해서 사용해줘야 한다. 

<report-list :reports="reports" :fields="fields" :click-handler="moveToReport"></report-list>

var ReportList = {
	Props: ['clickHandler']
};

new Vue({
  .. 
  methods:  {
    moveToReport: function() {
       ....
    }
  }
});

+ Recent posts