[입 개발] Redis 에서 zadd 와 zincrby 의 차이

안녕하세요. 입개발 CharSyam입니다. 오래간만에 포스팅을 하게 되네요. 오늘은 아주 간단한 것을 포스팅 할려고 합니다. 가끔씩 Redis의 sorted set 을 사용하는 명령중에 zadd 와 zincrby 가 있습니다. 과연 이 두 개의 명령은 어떤 차이가 있을까요?

결론부터 말하자면, zadd 와 zincrby 는 사실 같은 기능을 사용하는 아주 유사한 명령입니다. 먼저 zadd 는 다음과 같이 사용하니다.

zadd key score member

그러면 sorted set 에 member 가 해당 score를 가지게 됩니다. 여기서 가지게 됩니다를 중요하게 봐주세요.

그리고 zincrby 는 특정 값 increment 만큼 score를 증가시키는 명령입니다.

zincrby key increment member

그렇다면 다음 두 가지 경우로 나눠보도록 하겠습니다. 현재 해당 key에 member가 존재하지 않는 경우와 존재하는 경우입니다. 먼저 존재하지 않을 경우, 두 개의 명령 zadd 와 zincrby 는 모두 해당 member를 생성하게 됩니다. 그리고 zadd 는 해당 score로 설정하고 zincrby 는 0에 해당 increment를 추가한 것 처럼 동작합니다.(즉 둘 다 해당 값으로 설정하게 되는거죠.)

그렇다면 해당 member가 존재할 경우는 어떻게 될까요? zadd는 해당 member의 score를 현재 넘겨준 값으로 변경시켜 버립니다. 즉 기존에 3이 었는데 zadd key 1 member 라면 해당 member 의 score 는 3에서 1로 변경이 됩니다. zincrby 의 경우는 zincrby key 1 member 라면 기존 값이 3이 있었다면, 이제 3에 1을 더하게 되어서 4가 되게 됩니다.

코드를 보면 zadd 와 zincrby 는 Flag 하나만 다르고 동일한 함수를 사용합니다.

void zaddCommand(client *c) {
    zaddGenericCommand(c,ZADD_NONE);
}

void zincrbyCommand(client *c) {
    zaddGenericCommand(c,ZADD_INCR);
}

그리고 zaddGenericCommand를 보면 zsetAdd 를 호출하게 되는데… zadd는 모두 기존 값을 가져와서, score 를 비교합니다. 그래서 zincrby 에서 ZADD_INCR 설정되면 new_score 로 기존값 + increment로 설정하고, zadd 에서는 new_score 를 넘겨준 설정값으로 설정하게 됩니다.

            /* Prepare the score for the increment if needed. */
            if (incr) {
                score += curscore;
                if (isnan(score)) {
                    *flags |= ZADD_NAN;
                    return 0;
                }
                if (newscore) *newscore = score;
            }

            /* Remove and re-insert when score changed. */
            if (score != curscore) {
                zobj->ptr = zzlDelete(zobj->ptr,eptr);
                zobj->ptr = zzlInsert(zobj->ptr,ele,score);
                *flags |= ZADD_UPDATED;
            }

그리고 score가 변경되면 sorted set 은 기존 member를 지우고 다시 insert 하는 식으로 동작하게 됩니다. 결국 zadd 와 zincrby 는 거의 다른게 없습니다.

Advertisements

[입 개발] spring-security-oauth의 RedisTokenStore의 사용은 서비스에 적합하지 않습니다.

안녕하세요. 입개발 CharSyam 입니다. 저의 대부분의 얘기는 한귀로 듣고 한귀를 씻으시면 됩니다.(엉?) 일단 제목만 보면, 많은 Spring 유저들에게, 저넘의 입개발, 스프링도 모르면서라는 이야기를 들을듯 합니다.(아, 아이돌 까던 분들이 집단 따돌림을 당할 때의 느낌을 미리 체험할 수 있을듯 합니다. – 강해야 클릭율이 올라가는!!!)

먼저, 내용을 시작하기 전에 저는 Spring 맹, Java 맹으로 무식하다는 걸 미리 공개하고 넘어갑니다. 흑흑흑 (나의 봄님이 이럴 리 없어!!!)

일단 아시는 분이, Redis 와 oauth가 궁합이 안맞느냐라는 이야기를 들으면서 시작하게 됩니다. 아니, Redis와 oauth는 철자부터 다른데 무슨 얘기십니까? 라는 질문을 하다보니, Redis 에 과부하가 발생되서 처리가 잘 안된다는 얘기였습니다. 물론 수 많은 이유로 Redis가 느려질 수 있기 때문에, 자세한 정보를 요청했더니, spring-security-oauth 를 이용하고 계시다는 이야기를 들었습니다. 시간이 지나갈 수록 점점 느려진다는 느낌을 받고, 어떨 때는 Redis가 처리를 못한다고… 일반적인으로 아주 일반적으로 Redis는 아주 짧은 get/set 등의 요청은 초당 8~10만 정도는 가볍게 처리가 가능합니다.(일단 어떤식으로 문제에 접근했는가는 다음 번 주제로 미르고)

결론부터 얘기하자면, 당연히 사용하는 사람의 어느정도 실수가 발생하기 때문이긴 하지만, spring-security-oauth 의 RedisTokenStore 는 서비스에서 장애를 일으키기 쉽습니다.(보통은 라이브러리보다는 우리의 문제를 확인하는 것이 첫번째, 두번째도 우리문제, 세번째는 내 잘못인지 확인이… 정답입니다.)

먼저 Redis 는 Single Threaded 입니다. 즉, 하나의 명령이 많은 시간을 소모하면 그 동안은 아무런 작업을 하지 못합니다. 즉 Redis를 잘 쓰고 적합하게 이용하는 것은, 명령어를 빨리 수행해서 결과를 빨리 줄 수 있는 상황에서 이용하는 것이 적합하다는 것입니다.

사실 spring-security-oauth 자체가 큰 문제라기 보다는, 이 코드를 작성한 분은 사용자가 이런식으로 사용하게 되면 문제가 될 것이라는 고려를 하지 않은 것이 가장 큰 이슈입니다. 내부적으로 RedisTokenStore는 단순히 키 자체를 저장하는 get/set 커맨드와, 현재까지 발급되었던 키 정보를 저장하는 List 자료구조를 쓰고 있습니다. 아래 보면 rPush를 사용하는 approvalKey 와 clientId 입니다.

@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
......
    if (!authentication.isClientOnly()) {
	conn.rPush(approvalKey, serializedAccessToken);
    }
    conn.rPush(clientId, serializedAccessToken);
......
}

그런데 저 approvalKey와 clientId는 OAuth2Authentication -> OAuth2Request -> getClientId() 함수를 통해서 만들어집니다. 그런데 여기서 흔히들 하는 실수가 getClientId를 동일한 값으로 셋팅하는 것입니다. 그렇게 되면, 단순히 key가 있는지 확인할 때 말고 해당 List에 위와 같이 데이터가 들어가는 경우는 엄청나게 많은 아이템을 가진 List 자료구조가 생길 가능성이 높습니다.

그런데 Redis의 List 자료구조는 앞이나 뒤로 넣고, 앞이나 뒤에서 빼는 것은 빠르지만 O(1), 그 안의 데이터를 찾거나 모두 가져오면 결국 선형 탐색이 일어납니다. O(N), 그러면 그 안에 백만개 천만개가 들어있다고 가정하면 엄청난 속도 저하를 가져오게 됩니다. 그리고 그 안에 다른 명령을 하나도 처리할 수 가 없게됩니다.

아래 findTokensByClientIdAndUserName 와 findTokensByClientId 두 개의 함수는 대표적으로 모든 아이템을 가져오도록 하고 있습니다. 안쓰는걸로 하셔야 합니다.

	@Override
	public Collection findTokensByClientIdAndUserName(String clientId, String userName) {
		byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName));
		List byteList = null;
		RedisConnection conn = getConnection();
		try {
			byteList = conn.lRange(approvalKey, 0, -1);
		} finally {
			conn.close();
		}
		if (byteList == null || byteList.size() == 0) {
			return Collections. emptySet();
		}
		List accessTokens = new ArrayList(byteList.size());
		for (byte[] bytes : byteList) {
			OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
			accessTokens.add(accessToken);
		}
		return Collections. unmodifiableCollection(accessTokens);
	}

	@Override
	public Collection findTokensByClientId(String clientId) {
		byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId);
		List byteList = null;
		RedisConnection conn = getConnection();
		try {
			byteList = conn.lRange(key, 0, -1);
		} finally {
			conn.close();
		}
		if (byteList == null || byteList.size() == 0) {
			return Collections. emptySet();
		}
		List accessTokens = new ArrayList(byteList.size());
		for (byte[] bytes : byteList) {
			OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
			accessTokens.add(accessToken);
		}
		return Collections. unmodifiableCollection(accessTokens);
	}

위의 함수들은 안쓴다고 하더라도, 아래와 같이 removeAccessToken 함수는 자주 불리는데 여기서도 lRem을 통한 선형 탐색이 발생합니다.(lRem이 문제…)

	public void removeAccessToken(String tokenValue) {
		byte[] accessKey = serializeKey(ACCESS + tokenValue);
		byte[] authKey = serializeKey(AUTH + tokenValue);
		byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
		RedisConnection conn = getConnection();
		try {
			conn.openPipeline();
			conn.get(accessKey);
			conn.get(authKey);
			conn.del(accessKey);
			conn.del(accessToRefreshKey);
			// Don't remove the refresh token - it's up to the caller to do that
			conn.del(authKey);
			List results = conn.closePipeline();
			byte[] access = (byte[]) results.get(0);
			byte[] auth = (byte[]) results.get(1);

			OAuth2Authentication authentication = deserializeAuthentication(auth);
			if (authentication != null) {
				String key = authenticationKeyGenerator.extractKey(authentication);
				byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key);
				byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
				byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
				conn.openPipeline();
				conn.del(authToAccessKey);
				conn.lRem(unameKey, 1, access);
				conn.lRem(clientId, 1, access);
				conn.del(serialize(ACCESS + key));
				conn.closePipeline();
			}
		} finally {
			conn.close();
		}
	}

당연히 그러면 이건 사용자 실수가 더 큰거 아니냐 라고 하실 수 있습니다. 그런데 꼭 사용자의 실수가 아니더라도 특정 사용자가 비정상적으로 이런 동작을 하면 비슷한 이슈가 발생할 수 있습니다.

그러면 어떻게 해야하는가? 해당 로직들에서 list 를 사용하는 부분을 좀 더 속도가 빠르거나 하는 것들로 바뀌고 모든 데이터를 가져오는 부분은 사용하지 못하게 하는 것이 방법입니다.

어떤 분들은 이미 이런걸 겪으셔서 내부적으로 해당 모듈을 수정해서 쓰시는 분도 계셨습니다. 그러나 이런부분 때문에 관련 부분을 어느정도는 직접 구현해서 쓰시는 거나, 이런 부분을 수정해서 쓰셔야 할듯 합니다. 결론적으로 spring-security-oauth 의 RedisTokenStore를 쓰는 부분은 서비스 부하가 늘어날 때 장애를 일으킬 확률이 높습니다.

그 외에도 refreshToken의 경우는 보통 expire 기간이 훨씬 긴데, 이것들은 전부 메모리에 저장하는 부분들 또한 이슈가 생길 수 있습니다.(돈 많은면, 메모리 빵빵한 장비 쓰시면, 이 부분은 신경 안쓰셔도…)

보통 Spring이라는 이름을 붙이면 굉장히 안정적인데, 소스를 보고, 문제가 생길만한 부분에 대한 가정을 너무 약하게 하고 넘어간 부분이 문제인듯합니다.(하지만 해당 코드는 벌써 3년 전에 만들어진 코드라는 거…)

쉬시면서 Spring 코드 한번 읽어보시는 것도 좋을듯합니다.(전 읽어본 적 없습니다. ㅋㅋㅋ), 해당 글을 작성하는데 도움을 주신 우아한 형제들의 엯촋 이수홍 선생님께 감사드립니다.

[입 개발] 신묘한 Python locals() 의 세계

오늘도 약을 팔러온 입개발 CharSyam 입니다. 오늘은 지인 분께서, Python에서 locals() 함수를 쓰면 local 변수를 참조할 수 있는데, 특정 현상은 이해가 안된다고 얘기를 하셔서, 한번 왜 그럴까에 꽃혀서 찾아본 내용을 정리할려고 합니다. 참고로, Python 쓰시는데 이런게 있구나 빼놓고는 아마 하등의 도움을 못 받으실 내용이니, 조용히 뒤로 가기 버튼을 눌리시고, 생산적인 페이스북을 하시는게 더 도움이 되실꺼라고 미리 경고드립니다.

문제의 시작의 발단은 아래의 코드입니다.

def test(x):
    locals()['v'] = x + 10
    print(locals()['x'])
    return locals()['v']

print(test(100))

locals() 함수는 현재의 local 변수를 dict type으로 던져주는 내장 함수입니다.
그리고 사실 Python2, 3 문서에 보면 locals() 의 값은 고칠 수 없다라고 정의가 되어 있습니다.
https://docs.python.org/3.3/library/functions.html#locals
스크린샷 2018-05-04 오전 12.31.42

그런데 위에서 보면 locals()[‘x’]의 경우는 로컬 변수 x의 값을 가져오고, locals()[‘v’]를 하면 v가 추가가 됩니다. 즉 해당 변수안에서 새로운 값을 이 locals()에 추가하는 것은 된다는 것입니다. 그렇다고 해서 그냥 v로 쓸 수 있지는 않고 locals()[‘v’] 이런식으로만 사용이 가능합니다.

일단 locals()[‘v’] = 100 이라는 코드 자체는 locals()의 결과가 dict type이므로 전혀 문제가 없는 코드 입니다. 특별히 읽기 전용 속성 같은게 있는 것도 아니구요. 그리고 다시 locals()[‘v’]를 사용했을 때 해당 값을 가져올 수 있다는 것은, locals()가 같은 객체를 던져준다는 것입니다. id(locals()) 해보면 항상 같은 주소값을 던져줍니다.

즉 우리는 다음과 같은 가설을 세울 수 있습니다. 가정1) locals()에 값이 저장이 된다.
그럼 insert 가 되니 update 도 되지 않을까요? 당연히 아래와 같은 코드는 update도 잘됩니다. locals()의 결과는 dict type 의 객체일 테니…

def test(x):
    locals()['v'] = x + 10
    locals()['v'] = 1
    return locals()['v']

print(test(100))

그런데 다음과 같은 코드가 출동하며 어떨까요?

def test(x):
    locals()['x'] = 10
    return locals()['x']

print(test(100))

우리는 이미 답을 알고 있습니다. 처음 문서에 수정이 안된다고 했으니 당연히 결과는 100이 나오게 됩니다. 뭔가 이상하지 않나요? locals()의 결과는 그냥 dict type이고, 그래서 새로운 값을 추가하는 것도 분명히 되는데, 업데이트는 안됩니다. 뭔가 locals()는 특별하게 만든 기능이라, dict type 자체에서 set을 막고 있는 것일까요?

일단은 먼저 Python 2.7.14 기준으로 설명합니다. Objects/dictobject.c 의 PyDict_SetItem 함수를 보면 특별히 특정 속성일 때 쓰기를 막는다 이런코드는 보이지 않습니다. dict_set_item_by_hash_or_entry 안으로 들어가봐도 큰 차이는 없습니다.

int
PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
{
    register long hash;

    if (!PyDict_Check(op)) {
        PyErr_BadInternalCall();
        return -1;
    }
    assert(key);
    assert(value);
    if (PyString_CheckExact(key)) {
        hash = ((PyStringObject *)key)->ob_shash;
        if (hash == -1)
            hash = PyObject_Hash(key);
    }
    else {
        hash = PyObject_Hash(key);
        if (hash == -1)
            return -1;
    }
    return dict_set_item_by_hash_or_entry(op, key, hash, NULL, value);
}

그렇다면 dict type 자체의 이슈가 아니라 locals() 의 이슈가 아닐까라는 생각이 들게 됩니다. 제가 위에서 locals()는 내장함수라고 말씀드렸습니다. 그래서 Python/bltinmodule.c 를 살펴보면 builtin_locals를 찾을 수 가 있습니다. builtin_methods 테이블에 locals과 builtin_locals와 연결되어 있는 것을 찾을 수 있습니다.

static PyObject *
builtin_locals(PyObject *self)
{
    PyObject *d;
    d = PyEval_GetLocals();
    Py_XINCREF(d);
    return d;
}

그냥 PyEval_GetLocals() 만 호출합니다. Python/ceval.c 에 있습니다. 다시 따라가 봅시다.

PyObject *
PyEval_GetLocals(void)
{
    PyFrameObject *current_frame = PyEval_GetFrame();
    if (current_frame == NULL)
        return NULL;
    PyFrame_FastToLocals(current_frame);
    return current_frame->f_locals;
}

뭔가 이름이 있어 보이는 PyFrame_FastToLocals 함수가 보입니다. current_frame 은 뭔지는 잘 모르겠지만, locals니, 특정 scope에서의 코드 정보(함수안이냐, 글로벌이냐?)를 가져오는 걸로 예측이 됩니다.

다시 PyFrame_FastToLocals 를 따라가 봅시다. Objects/frameobject.c 에 존재합니다.

void
PyFrame_FastToLocals(PyFrameObject *f)
{
    /* Merge fast locals into f->f_locals */
    PyObject *locals, *map;
    PyObject **fast;
    PyObject *error_type, *error_value, *error_traceback;
    PyCodeObject *co;
    Py_ssize_t j;
    int ncells, nfreevars;
    if (f == NULL)
        return;
    locals = f->f_locals;
    if (locals == NULL) {
        locals = f->f_locals = PyDict_New();
        if (locals == NULL) {
            PyErr_Clear(); /* Can't report it 😦 */
            return;
        }
    }
    co = f->f_code;
    map = co->co_varnames;
    if (!PyTuple_Check(map))
        return;

    PyErr_Fetch(&error_type, &error_value, &error_traceback);
    fast = f->f_localsplus;
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;

    if (co->co_nlocals)
        map_to_dict(map, j, locals, fast, 0);
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    fprintf(stderr, "co_nlocals: %d, ncells : %d, nfreevars : %d\n", co->co_nlocals, ncells, nfreevars);
    if (ncells || nfreevars) {
        fprintf(stderr, "map_to_dict1()\n");
        map_to_dict(co->co_cellvars, ncells,
                    locals, fast + co->co_nlocals, 1);
        /* If the namespace is unoptimized, then one of the
           following cases applies:
           1. It does not contain free variables, because it
              uses import * or is a top-level namespace.
           2. It is a class namespace.
           We don't want to accidentally copy free variables
           into the locals dict used by the class.
        */
        if (co->co_flags & CO_OPTIMIZED) {
            fprintf(stderr, "map_to_dict2()\n");
            map_to_dict(co->co_freevars, nfreevars,
                        locals, fast + co->co_nlocals + ncells, 1);
        }
    }
    PyErr_Restore(error_type, error_value, error_traceback);
}

위의 코드를 보면 f->f_locals 를 locals에 대입하고 없으면 dict type을 생성하는 것을 알 수 있습니다. 우리의 locals()의 결과는 dict type이고 이름까지 비슷하니 이넘이구나 하실껍니다. 그런데 왜 update는 안되는 것일까요?

그 의문은 다음 블로그에서… 는 뻥이고… co->co_nlocals 라는 변수에 있습니다. co는 codeobject 의 약어로 보이고, codeobject 는 해당 함수 관련 정보(global 변수 정보, local 변수 정보)를 가지고 있는 것으로 보입니다.(대충 봐서) 그 중에서 co_nlocals 는 local 변수의 개수를 가지고 있습니다. 즉 local 변수가 한개라도 있으면 map_to_dict 이 실행됩니다. 저는 처음에는 map_to_dict 에서 뭔가 locals 를 만들어 주는 줄 알았습니다. 그런데 map_to_dict 함수를 보면 map 에 있는 key를 dict에다가 PyObject_SetItem 를 통해서 덮어씌워버립니다. 그리고 저 map은 코드가 실행될때 넘어온 파라매터나 로컬 변수의 값들이 저장되어 있는 상황입니다.

static void
map_to_dict(PyObject *map, Py_ssize_t nmap, PyObject *dict, PyObject **values,
            int deref)
{
    Py_ssize_t j;
    assert(PyTuple_Check(map));
    assert(PyDict_Check(dict));
    assert(PyTuple_Size(map) >= nmap);
    for (j = nmap; --j >= 0; ) {
        PyObject *key = PyTuple_GET_ITEM(map, j);
        PyObject *value = values[j];

        assert(PyString_Check(key));
        if (deref) {
            assert(PyCell_Check(value));
            value = PyCell_GET(value);
        }
        if (value == NULL) {
            if (PyObject_DelItem(dict, key) != 0)
                PyErr_Clear();
        }
        else {
            if (PyObject_SetItem(dict, key, value) != 0)
                PyErr_Clear();
        }
    }
}

헉, 하고, 이해를 하신 분들이 이미 계실꺼 같지만, 넵 그렇습니다. locals()를 호출할 때 아까 x의 값이 안바뀐 이유는 실제로 바뀌어도 locals()를 다시 호출할 때 map_to_dict 함수를 통해서 원래의 값으로 덮어씌워지기 때문입니다. 그래서 다른값들은 같은 locals를 사용하므로 추가나 변경, 삭제도 가능하지만, 기존에 존재하는 값들은 다시 원래의 값으로 덮어씌워지기 때문입니다.

def test(x):
    x = 15
    locals()['x'] = 1
    return locals()['x']

print(test(100))

그래서 local 변수면 동일하게 동작하므로 파라매터가 아니고 그냥 내부에서 미리 선언된 local 변수라도 이렇게 값이 바뀌지 않습니다.

def test():
    x = 15
    locals()['x'] = 1
    return locals()['x']

print(test())

우와 Python 의 locals()는 참 신묘합니다. 그런데… 문제는 여기서 끝이 아닙니다.(아니 도대체 쓸데 없는 이야기를 얼마나 더 할려고?) 그냥 여기까지 이해하고 넘어가려고 하는데… PyFrame_FastToLocals 함수 밑과 위에 map_to_dict 말고 dict_to_map 과 PyFrame_LocalsToFast 라는 함수가 있는게 아니겠습니까?

먼저 dict_to_map 함수입니다.

static void
dict_to_map(PyObject *map, Py_ssize_t nmap, PyObject *dict, PyObject **values,
            int deref, int clear)
{
    Py_ssize_t j;
    assert(PyTuple_Check(map));
    assert(PyDict_Check(dict));
    assert(PyTuple_Size(map) >= nmap);
    for (j = nmap; --j >= 0; ) {
        PyObject *key = PyTuple_GET_ITEM(map, j);
        PyObject *value = PyObject_GetItem(dict, key);
        assert(PyString_Check(key));
        /* We only care about NULLs if clear is true. */

        if (value == NULL) {
            PyErr_Clear();
            if (!clear)
                continue;
        }
        if (deref) {
            assert(PyCell_Check(values[j]));
            if (PyCell_GET(values[j]) != value) {
                if (PyCell_Set(values[j], value) f_locals into fast locals */
    PyObject *locals, *map;
    PyObject **fast;
    PyObject *error_type, *error_value, *error_traceback;
    PyCodeObject *co;
    Py_ssize_t j;
    int ncells, nfreevars;
    if (f == NULL)
        return;
    locals = f->f_locals;
    co = f->f_code;
    map = co->co_varnames;
    if (locals == NULL)
        return;
    if (!PyTuple_Check(map))
        return;
    PyErr_Fetch(&error_type, &error_value, &error_traceback);
    fast = f->f_localsplus;
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;
    if (co->co_nlocals)
        dict_to_map(co->co_varnames, j, locals, fast, 0, clear);
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    if (ncells || nfreevars) {
        dict_to_map(co->co_cellvars, ncells,
                    locals, fast + co->co_nlocals, 1, clear);
        /* Same test as in PyFrame_FastToLocals() above. */
        if (co->co_flags & CO_OPTIMIZED) {
            dict_to_map(co->co_freevars, nfreevars,
                locals, fast + co->co_nlocals + ncells, 1,
                clear);
        }
    }
    PyErr_Restore(error_type, error_value, error_traceback);
}

다음은 PyFrame_LocalsToFast 함수입니다.

void
PyFrame_LocalsToFast(PyFrameObject *f, int clear)
{
    /* Merge f->f_locals into fast locals */
    PyObject *locals, *map;
    PyObject **fast;
    PyObject *error_type, *error_value, *error_traceback;
    PyCodeObject *co;
    Py_ssize_t j;
    int ncells, nfreevars;
    if (f == NULL)
        return;
    locals = f->f_locals;
    co = f->f_code;
    map = co->co_varnames;
    if (locals == NULL)
        return;
    if (!PyTuple_Check(map))
        return;
    PyErr_Fetch(&error_type, &error_value, &error_traceback);
    fast = f->f_localsplus;
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;
    if (co->co_nlocals)
        dict_to_map(co->co_varnames, j, locals, fast, 0, clear);
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    if (ncells || nfreevars) {
        dict_to_map(co->co_cellvars, ncells,
                    locals, fast + co->co_nlocals, 1, clear);
        /* Same test as in PyFrame_FastToLocals() above. */
        if (co->co_flags & CO_OPTIMIZED) {
            dict_to_map(co->co_freevars, nfreevars,
                locals, fast + co->co_nlocals + ncells, 1,
                clear);
        }
    }
    PyErr_Restore(error_type, error_value, error_traceback);
}

PyFrame_FastToLocals 가 원래 locals() 를 호출할 때 실제로 수행되는 함수였다면 PyFrame_LocalsToFast 는 뭔가 역으로 값을 바꿀 수 있는 함수가 아닐까라는 생각을 해봤습니다. 해당 함수를 역으로 살짝 추적해보니(악, 여기서 그만뒀어야 하는데!!!) Python/ceval.c 파일안에 exec_statement 라는 함수에서 이걸 호출해줍니다.

설마설마 하면서 다음과 같은 예제를 만들어봤습니다.

def a(x):
    exec("locals()['x'] = 100")
    print(x)
    return locals()['x']

print(a(10))

python 으로 실행을 시키니 그냥 10만 나옵니다. 안되나? 라고 생각했는데… 제가 보던 소스는 Python 2.7.14이고, 제가 실행시킨 python은 3.x(이래서 머리가 무식하면 손발이 고생합니다.), 다시 python2 버전으로 돌려보시면 짜잔, 값이 100이 나옵니다. 즉 우리는 로컬 변수 x를 덮어씌운겁니다. –-; 그런데 Python3를 살펴보면 이 exec_statement 라는 함수가 사라지고 실제로 PyFrame_LocalsToFast를 호출하기 힘든 형태로 바뀌었습니다. –-;(망할… 즉 python3 에서는 동작하지 않고 python2에서만 실행이 됩니다.)

심지어 이렇게 하면 이런 이상한짓도 가능합니다. 아까는 locals()는 그냥 dict_type이라 뭔가 추가해도 다시 dict에서 꺼내와야 했는데… 아래의 예제를 사용하면 local 변수의 생성도 가능합니다. -_-(그런데 이렇게 생성할 필요가…, 참고로 Python3에서는 실행조차 안됩니다.)

def a(x):
    exec("locals()['x'] = 100")
    exec("locals()['y'] = 10")
    print(x)
    print(y)
    return locals()['x']

print(a(10))

어쩌다 보니, 소스를 파다보니 이런 내용을 알게 되었는데, 사실 사용하실 때는 1의 도움도 안되는 뻘 글을 읽어주셔서 감사합니다.

[입 개발] 전문가는 계속 공부하는 사람이다. – 김창준님의 개발자 실력 평가 어떻게 할 것인가 후기

안녕하세요. 입개발 CharSyam입니다. 둘째가 100일을 넘어서 저녁 약속 없는 신데렐라 시간을 하다가, 김창준님이 “개발자 실력 평가 어떻게 할 것인가?” 에 대해서 강의하신다고 해서 마님에게 애교를 부리면 허락을 받고 세미나를 들으러 왔습니다.

평소 입개발자는 입개발의 단계를 올리기 위해서 부단히 노력해야 하며, 혀로 키보드 치기, 입에 발린 소리하기등을 연습해야 하는데, 개발자 실력 평가에서 어떻게 하면 잘 빠져나갈 수 있을 것인가에 대한 힌트를 얻기 위해서 겨우겨우 참여하게 되었습니다.

웬지 창천항로님이 어마어마한 후기 를 이런식으로 남겨주실 것 같아서, 저는 느낌만…(참고로 저 링크는 창천항로님이 다른 세미나를 듣고 쓰신 후기… 대박!!! – 벌써 오늘 후기를)

일단 요약부터 하자면, 개발자 실력이라고 적었지만, 전문가를 판별하는 방법은 질문(소통)과 공부를 하는 사람이라는 것이 핵심이었습니다. 먼저 코딩 테스트의 비효율성, 코딩 테스트로 테스트를 하면 코딩 테스트만 잘 푸는 사람이지, 회사의 업무를 잘 할 사람일 가능성과는 별 개의 일이라는 이야기… 전문가를 구분하는 테스트를 할 때, 기존의 테스트의 잘못된 가정으로 인해서(요새 통계학 쪽에서 말이 많은 P-Value 처럼), 지금까지는 비용이나 표본의 이슈등으로, 짧은 시간에 풀 수 있는 문제와, 혼자서 풀 수 있는 문제가 많았는데, 긴 시간을 들여야 하는 문제나, 협업해야 하는 문제의 경우, 정말 전문가는 다른게 문제를 인지하고 해결한다는 것이었습니다.

또한 전문가는 컨텍스트를 이해하고 적용할려고 하는 반면에, 초보자는 단순히 문제를 풀려고 하는데, 제출되는 문제들의 경우는 컨텍스트가 제거되고 단순히 어떤 결과만을 바라는 문제들이라, 이걸 풀었다고 해서, 정말 일을 잘하는 지는 알 수 없다라는 얘기가 나왔습니다.(아, 제가 이해하고, 기억하는게 맞는건지… 애매하네요.)

그럼 일단 전문가는 어떻게 알 수 있는가? 삼각측량 처럼, 다양한 평가(동료평가, 상사평가, 버그생성율, 코드 리뷰, 디자인 리뷰) 등을 거쳐서 점수가 골고루 높은 사람은 전문가일 가능성이 높은데, 뽑을 때는 이런 평가를 해서 뽑을 수는 없습니다.

그렇다면, 우리회사에 적합한 사람은 어떻게 뽑을 것인가? 실제로 할 일을 비슷하게 만들어서, 이런 일을 해보도록 시키는것, 다만, 이를 위해 회사에서 실제로 잘하는 개발자, 평범한 개발자 그룹을 만들어서 비슷한 시험을 보게해서 잘하는 개발자들은 어떤 특징을 가져야 하는지를 찾아야 한다고 합니다. 그리고 이를 평가할 때, 누구나 비슷한 기준이 나오도록 기준을 정하는게 중요한데, 단순히 pass, fail이 아니라, 점수로 표현을 해야 한다고 하네요.
그리고 이 채점 기준은, 잘하는 사람들과 평범한 사람들의 그룹에서 나오는 평균적인 행동의 차이(예를 들어, 잘하는 그룹은 평균적으로 질문을 5회 이상한다. 등의 기준을 찾아내야 한다고 합니다.)

이걸 들으면서 생각난게 피보탈랩스의 입사 시험 방식입니다.(전 본적은 없고 듣기만…) 해당 팀의 업무를 모두 하루나 길면 이틀 단위로 나누고, 실제로 입사자와 해당 태스크를 직접 구현하고, 가능하면 배포까지 하는 것이 면접이라고 합니다. 팀의 새로운 툴에 대한 이해도나 커뮤니케이션 능력, 적응력을 다 볼 수 있는 테스트라고 하네요. 다만 다른 회사 분들과 얘기를 해보면, 소스 코드의 유출등이 가능하고, 시간을 너무 들여야 해서 어렵울 것 같다고 하시던데, 오픈소스 회사는 이런게 또 가능할 듯 합니다.

회사 내에 해당 분야의 전문가가 있을 때는 기술력 검증이 쉽겠지만, 그렇지 않을 경우는 어떻게 할 것인가라는 질문이 있었는데, 전문가이고 잘할수록 더 열심히 공부하는 경향이 있다고 하네요.

이제 여러분도 이런걸 명심하시고 대비하시면 좋은 입개발러가 되실 수 있습니다.(엉?)

[입 개발] Memcrashed DDOS에 대해서 살짝 아는척 해봅시다.

우와, 최근에 CloudFlare 에서 아주 재미있는 제목으로 글을 냈습니다. 제목은 Memcrashed – Major amplification attacks from UDP port 11211 로, UDP 11211 포트를 이용한 대규모 DDOS 어택 정도로 생각하시면 될듯합니다.(영어를 못해서 의역으로…)

사실 원문을 보시는게 더 쉽게 이해하실 듯 하지만(당연히 원문 처럼 설명한 능력도 없지만…) public 에 포트가 열려있는 memcached 를 이용한 DDOS 방법입니다. 그래서 제목이 memcrashed 가 된거죠. memcached는 웹서비스 쪽에서는 누구나 알고 있는 유명한 In-Memory caching 솔루션입니다.(http://memcached.org/) 성능도 아주 끝내주죠. 간단한 연산은 초당 10~20만 까지도 가능합니다.

memcrashed

먼저 해당 이슈는 일단 다음과 같은 전제조건이 모두 만족해야 DDOS 공격이 가능합니다..

  • memcached 가 public 하게 열려있다. – 사실 절대로 해서는 안되는 행위입니다.
  • memcached 가 udp 포트를 열고 있다.
  • 그리고 memcached 자체가 DDOS 공격의 대상이 아니라, 공격을 할 수 있는 수단으로 사용됩니다.

    저도 몰랐던 사실인데(나름 memcached는 그래도 아는편이라고 생각했는데…) memcached는 UDP도 지원합니다.(당연히 UDP다 보니, 명령이나 응답이 유실될 수 도 있습니다.) 원래는 명시적으로 -U 0 을 주지 않는 이상은 UDP 11211 포트로 생성됩니다. memcached 1.5.5 버전을 받아서 설치하고 실행해보면 자동으로 IPv4(TCP, UDP), IPv6(TCP, UDP) 11211 포트가 열리는 것을 볼 수 있습니다.

    memcached_1_5_5_port

    즉 udp로 명령을 보내고 사용할 수 있다는 거죠. UDP 프로토콜은, 기존 TCP 프로토콜과 거의동일하지만 아주 미세한 차이가 있습니다.

    그런데 UDP의 경우에는 source ip를 위조하는 것에 굉장히 취약합니다.(IP Spoofing), TCP도 불가능한건 아니지만 훨씬 더 어렵습니다. 여기서 자세한 설명 없이 ip header 와 udp header를 첨부합니다.

    ip_header

    udp_header

    IP Spoofing 을 통해서 데이터를 보내게 되면 memcached 입장에서는 송신자를 체크할 방법이 없습니다. 그래서 그 응답결과를 송신자(로 속여진 victim) 에게 보내게 되는겁니다. 엄청나게 UDP 패킷이 전송될 수 있겠죠. 실제로 memcached는 디폴트로 1MB chunk를 사용하므로 데이터는 한번에 1MB 까지 가능합니다. 이렇게 열려있다는 것은 거기에 자신이 원하는 데이터도 심을 수 있으니…(다만 UDP로 1MB 데이터를 넣기는 힘들겠지만… 이 얘기는 TCP 도 열려있고 방화벽이 없을 가능성이 높으니… 원하는 데이터를 쉽게 넣을 수 있을듯 합니다.) 초당 엄청난 트래픽을 보낼 수 있게 됩니다.

    실제로 간단하게 셋팅을 해보았습니다. 간단하게 외부의 victim 에서 응답이 수신되는 걸 확인 할 수 있었습니다.

    spoof.png

    원문을 보면 nmap등을 이용해 간단하게 public 에 열려있는 memcached 서버들을 찾을 수 있습니다. 무시무시하지요.

    그럼 결론, 우리는 어떻게 대비해야 하는가?
    1] memcached를 public 에 공개하지 않는다. 이미 열려있는 곳이라면 iptable 등으로 방화벽을 따로 설정해서 UDP및 TCP 자체를 막으셔야 합니다. 이런 캐시서버는 public에 열리면 그냥 지옥입니다. redis의 경우도 바로 해당 계정이 탈취 당할 수 있습니다.(udp는 지원안합니다만…)

    2] UDP를 안 쓰면 사용하지 않는다.
    memcached 1.5.5 까지는 -U 0 라는 옵션을 주지 않으면 자동으로 UDP 11211 포트를 사용하였지만… 해당 이슈 이후에 긴급하게 나온 memcached 1.5.6은 UDP가 디폴트로 꺼져있습니다. 그러나 아마 대부분은 이전 버전을 쓰실거니… 시작 옵션도 미리 바꿔두시는게 좋습니다. 다음 memcached patch 를 보시면 UDP가 이제 디폴트로 disable 된걸 볼 수 있습니다.

    보안 관련 내용이라, 사용된 소스나 자세한 정보는 적지 않습니다.(다만 엄청 쉬워요 T.T)

    ps. 보안 이슈라는 것이, 방비를 잘 하더라도 안당한다고 말할 수 없지만, 대부분의 보안 이슈는, 사용하지 않는 서비스를 public 에 노출한다거나, 잘못된 설정으로 인해서 발생하는 경우가 많습니다. memcached 이슈도 마찬가지이고, S3에 대한 리포트 를 보셔도 실제로 전체 s3 버킷의 20%가 쓰기도 열려있다라는 충격적인 사실을 아실 수 있습니다. 큰 조직에서는 이런 문제를 전담해줄 만한 인력이 있지만, 중소 규모 사이즈에서는 더 신경을 많이 쓰셔야 합니다.

    [입 개발] SipHash의 사용, Data DDOS를 방지해볼까?

    Python 3.3 부터는 내장 hash 함수가 RANDOM SEED를 이용하는 방식으로 바뀌었고, 내부 함수도 내부적으로 SipHash 를 쓰도록 바뀌었고, Redis 에서도 4.x 부터는 내부 해쉬 방식이 SipHash 를 쓰는 것으로 바뀌었습니다.

    그러면 잘 쓰고 있던(?) 기존의 hash를 왜 siphash라는 구조로 바꾸는 것일가요? 아, 일단 먼저 고백할 것은 전 siphash 가 뭔지는 잘 모릅니다. siphash 홈페이지를 방문하면 다음과 같은 내용을 볼 수 있습니다.

    SipHash is a family of pseudorandom functions (a.k.a. keyed hash functions) optimized for speed on short messages.

    Target applications include network traffic authentication and defense against hash-flooding DoS attacks.

    SipHash is secure, fast, and simple (for real):
    SipHash is simpler and faster than previous cryptographic algorithms (e.g. MACs based on universal hashing)
    SipHash is competitive in performance with insecure non-cryptographic algorithms (e.g. MurmurHash)
    We propose that hash tables switch to SipHash as a hash function. Users of SipHash already include FreeBSD, OpenDNS, Perl 5, Ruby, or Rust.

    The original SipHash returns 64-bit strings. A version returning 128-bit strings was later created, based on demand from users.

    Intellectual property: We aren’t aware of any patents or patent applications relevant to SipHash, and we aren’t planning to apply for any. The reference code of SipHash is released under CC0 license, a public domain-like license.

    뭔지 잘 모르겠지만, 자애로운 구글신을 영접하면 다음과 같이 번역되어 나옵니다.(아휴… 이제 번역은 구글느님이시죠.)

    SipHash는 단문 메시지의 속도에 최적화 된 의사 난수 함수 (a.k.a. 키 해시 함수)의 계열입니다.

    대상 응용 프로그램에는 네트워크 트래픽 인증 및 해시 넘침 DoS 공격 방어가 포함됩니다.

    SipHash는 안전하고 빠르며 간단합니다 (실제).
    SipHash는 이전의 암호화 알고리즘 (예 : 범용 해시 기반의 MAC)보다 간단하고 빠릅니다.
    SipHash는 안전하지 않은 비 암호화 알고리즘 (예 : MurmurHash)
    우리는 해시 테이블을 해시 함수로 SipHash로 전환 할 것을 제안합니다. SipHash 사용자는 이미 FreeBSD, OpenDNS, Perl 5, Ruby 또는 Rust를 포함합니다.

    원래 SipHash는 64 비트 문자열을 반환합니다. 128 비트 문자열을 반환하는 버전이 나중에 사용자의 요구에 따라 만들어졌습니다.

    지적 재산권 : 우리는 SipHash와 관련된 특허 또는 특허 출원을 모르고 있으며, 신청할 계획이 없습니다. SipHash의 참조 코드는 공개 도메인과 같은 라이센스 인 CC0 라이센스에 따라 릴리스됩니다.

    그럼 왜 이런걸 사용하는가라고 한다면, 다음과 같은 예를 하나 들어보려고 합니다. Java 7이나 .NET의 Set 자료구조를 보면, 실제로 내부에는 Hash 자료구조를 사용하고 있습니다.(당연하지!!! 그것도 몰랐냐 이넘아 하시면… 전… 굽신굽신) Set이라는 자료구조는 특정 item 이 존재하는지 아닌지를 상수시간(이라고 적고 빠른 시간에) 확인할 수 있는 자료구조입니다. 그런데 Hash는 보통 충돌이라고 불리는 Hash 값이 겹칠 수 밖에 없는 경우가 존재하고 이를 막기 위해서, 링크드 리스트를 이용한 이중 체인 같은 것을 많이 사용합니다.

    즉 다음 그림과 같은 구조가 됩니다. 일단 다음 그림은 아주 이상적으로 Hash 가 하나씩 차지한 경우이구요.
    siphash1

    다음은 이제 삐꾸가 나서 한 hash slot 에만 비정상적으로 몰리는 경우입니다.
    siphash2

    그런데 지금 이러한 내용을 왜 말하는가 하면, 이런 특성을 이용해서 특정 서비스에 DOS(Denial of Service) 서비스를 할 수 있다는 것입니다.(아니 자네, 지금 무슨 말을 하는 것인가?)

    우리가 흔히 아는 DOS 또는 DDOS는 무수한 클라이언트를 이용해서, 특정 사이트에 접속을 시도하거나 해서, 서비스를 못할 정도로 네트웍을 사용하거나, 특정 서비스에 시간이 오래걸리는 무거운 작업을 하도록 하여서, 서비스를 하지 못하게 하는 공격입니다.

    자, 여기서 힌트를 얻으신 분들이 있으실지도 모르겠습니다. 앞에서 말한 Hash를 바꾼 이유와, 시간이 오래걸리는 무거운 작업을 하도록 한다를 섞으면… 설마라고 생각하시는 분들이 계실텐데… 넵 바로 그 이유입니다.(전 사실 그 이유를 모르죠!!! 퍽퍽퍽…)

    자, 만약에 특정 사이트에서 어떤 컴포넌트를 이용하는 걸 알고 있습니다. 그리고 그 툴에서 자료구조를 어떻게 처리하는 지도 안다면? 예를 들어, Java 7의 Set 이나 HashMap 을 사용하는 걸 알고, 거기에 데이터를 넣을 것이라는 걸 안다면…(오픈소스들이 위험할 수 있습니다.) 특정 패턴의 Key를 넣는 것으로, Hash 검색 속도를 미친듯이 느리게 만들 수 있습니다.

    한 곳에 데이터가 몰리는 것을 skew 라고 하고 이진 트리등에서도 skew 가 되면 엄청 느린 검색 속도를 보여주게 되는데.. 위의 그림 처럼, 충돌이 일어날 경우에 linked list를 이용한 체이닝으로 문제를 푼다면, 거기에만, 천개, 만개, 십만개가 있다면, 이제 해당 슬롯에 있는 key를 조회하는 명령이 들어오면, 속도는 점점 느려지게 될 것입니다.(정말?)

    먼저 java7 에서의 HashMap에서 hash 함수를 확인해보도록 하겠습니다.(왜 자바7이냐 하면 자바8에서는 HashMap이 충돌시에 Tree 형태로 저장되게 됩니다. – 그러나 이것도 효율적이긴 하지만, 정말 많은 데이터를 한 곳에 넣으면… 문제의 소지가!!!)

    java7 에서의 HashMap 에서 사용하는 hash 함수는 다음과 같습니다.

        static int hash(int h) {
            // This function ensures that hashCodes that differ only by
            // constant multiples at each bit position have a bounded
            // number of collisions (approximately 8 at default load factor).
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }
    

    hash(key.hashCode()) 이런식으로 이용하게 되는데 만약에 key가 string class의 경우 hashCode() 함수는 다음과 같습니다.

        public int hashCode() {
            int h = hash;
            if (h == 0 && value.length > 0) {
                char val[] = value;
    
                for (int i = 0; i < value.length; i++) {
                    h = 31 * h + val[i];
                }
                hash = h;
            }
            return h;
        }
    

    즉 특정 string 의 hash table의 위치는 항상 그대로 입니다. 이걸 이용해서 같은 해쉬 슬롯에 들어가는 데이터를 대량으로 던져준다면?(사실 이게 쉽지는 않습니다., 어떤 key를 추가하게 할 것인가라는게 사용자 입장에서는 접근할 여지가 적은… 하지만… 가능하다면?) 물론 java7에서도 아이템 개수가 커지면 Table을 키우게 되긴 합니다만… 이것 역시 동일한게 만들어주면… 사실 더 최악인 메모리는 계속 팩터 만큼 커지는데… 아이템은 계속 같은 곳에 몰리는 형상이 벌어질 수 있습니다.(이걸 의도한 공격이죠.)

    실제로 java7 에서는 resize(), transfer() 함수가 테이블 확장을 하게됩니다.

        void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
    
            Entry[] newTable = new Entry[newCapacity];
            transfer(newTable);
            table = newTable;
            threshold = (int)(newCapacity * loadFactor);
        }
    
        void transfer(Entry[] newTable) {
            Entry[] src = table;
            int newCapacity = newTable.length;
            for (int j = 0; j < src.length; j++) {
                Entry<K,V> e = src[j];
                if (e != null) {
                    src[j] = null;
                    do {
                        Entry<K,V> next = e.next;
                        int i = indexFor(e.hash, newCapacity);
                        e.next = newTable[i];
                        newTable[i] = e;
                        e = next;
                    } while (e != null);
                }
            }
        }
    

    그럼 siphash를 쓰면 어떤 부분에서 도움이 될까요? 위에서 보여준 java7에서의 hash 나 python의 기존방식이나, redis 3.x대의 hash 또는 MD5, SHA1, SHA256 계열의 경우, 우리가 알듯이 항상 특정 key 의 hash는 같은 알고리즘에서 항상 같은 결과가 나옵니다.(consistent hashing 같은 경우는 이런 특성을 이용한 방식이죠.)

    그런데 siphash 종류의 hash는 seedkey라는 것을 hash 시에 추가로 받습니다. 그리고 이 seedkey로 인해 hash 결과가 바뀌게 됩니다. 그럼 이걸 어떻게 해야하는가? 예를 들어 프로세스가 시작되는 타이밍에 저 seedkey를 랜덤으로 생성합니다. 그러면, 해당 프로세스 내에서는 항상 동일한 값을 보장하지만, 새로운 프로세스가 뜨면, 동일한 key에 다른 hash값을 내놓을 것입니다.(물론 해당 프로세스 내에서는 항상 동일하겠죠.) 즉 어떤 서버에서 실행될 때, 이 key가 어떤 위치에 위치할지를 알 수 없게 됩니다. 그로 인해서 위의 알고리즘을 알더라도, 같은 슬롯에 충돌을 유도하기가 힘들어집니다. 다음은 Redis에서 패치된 코드입니다. 기본 hash가 내부적으로 siphash를 쓰도록 되어 있습니다.

    int main(int argc, char **argv) {
        ......
        getRandomHexChars(hashseed,sizeof(hashseed));
        dictSetHashFunctionSeed((uint8_t*)hashseed);
        ......
    }
    
    uint64_t dictGenHashFunction(const void *key, int len) {
        return siphash(key,len,dict_hash_function_seed);
    }
    

    비슷한 이유로 Python 에서도 3.3 이후로는 기본으로 RANDOM SEED와 siphash류를 쓰도록 되어있습니다.

    [입 생활] aws, github, 2FA 활성화나 수정 방법

    이제 점점 더 귀찮아지지만 2 Factor Authentication 이 거의 필수처럼 여겨지고 있습니다. aws도 그렇고, github도, organization 에서 2FA가 안켜져 있으면 관리자가 계정 다 삭제해 버릴수도 있습니다. ㅋㅋㅋ

    그런데 핸드폰은 2년마다 고장나고…(제껀 3년 만에…) 이럴 경우 이런 2FA를 바꿔야 하는데 aws나 github이나 다 쉽게 가능합니다만… -_-;;; 제가 워낙 바보라 저장해둡니다.

    0] OPT 앱

    구글 AUthentication 앱 사용

    1] aws

    my security credentials -> Multi-factor authentication (MFA) 를 선택해서 기존꺼를 삭제하고 추가하시면 됩니다.

    2] Github

    Settings -> Security -> Two-factor authentication -> Edit -> Delivery Option -> Reconfigure two-factor authentication -> Set up using an app -> Next 하면 바코드가 뜨니 이걸 OPT 앱으로 저장하면 됩니다.
    이때 Next 가 비활성화 되어 있는데, 위의 Download, print, copy 중에 하나를 하시면 됩니다.