저는 Couchbase 클라이언트에 대한 Twisted 인터페이스를 작업 중입니다(https://github.com/couchbase/couchbase-python-client). 이 링크는 동기식 인터페이스를 가리킵니다. 실험용 트위스트 브랜치는 https://github.com/couchbaselabs/couchbase-twisted-client
Twisted 클라이언트의 작동 방식을 설명하기 위해 내부적으로 C 확장이 어떻게 작동하는지 조금 설명하겠습니다.
기본 SDK 내부
libcouchbase: 크로스 플랫폼 라이브러리
동기식 클라이언트 자체는 C 확장을 사용하여 libcouchbase (https://github.com/couchbase/libcouchbase). libcouchbase는 다양한 다른 언어(예: Ruby, PHP, node.js, Perl 및 기타 여러 언어)로 작성된 여러 다른 Couchbase 클라이언트의 공통 플랫폼 레이어 역할을 합니다.
파이썬 확장 프로그램은 파이썬의 C API를 사용하는 C 확장 프로그램입니다(ffi, Cython 등을 사용하지 않는 이유는 다른 논의의 문제이지만, 지금까지 파이썬의 C API와 확장 프로그램의 전반적인 안정성 및 유지 관리에 매우 만족해 왔다고만 말씀드리겠습니다).
확장 기능이 작동하는 방식은 상당히 복잡합니다. Python에 표시되는 노출된 API는 다양한 데이터 액세스 방법을 제공합니다(예 set, get, 추가, 추가, 삭제와 같이 여러 값에 대해 작동할 수 있는 변형뿐만 아니라 set_multi, get_multi 등). 이러한 메서드는 파이썬 확장으로 직접 호출하여 일부 인수 유효성 검사를 수행한 다음 이러한 작업을 libcouchbase로 스케줄링합니다(예 lcb_set(), lcb_get()등). 이러한 작업이 예약되면 내선 번호는 다음을 호출합니다. lcb_wait() 예약된 작업의 결과가 완료될 때까지(또는 오류로 실패할 때까지) 차단합니다.
일반적인 코드는 다음과 같습니다:
LCB_GET_CMD_T *cmdp = &명령;
명령을 사용합니다.v.v0.키 = 키; /* 여기서 '키'는 일부 파이오브젝트에서 추출한 것입니다 */.
명령을 사용합니다.v.v0.nkey = nkey; /* 키의 길이 */
명령을 사용합니다.v.v0.예외 시간 = 예외 시간; /* 선택 사항, 매개변수에서 추출 */
오류 상태 = lcb_get(인스턴스, /* 라이브러리 핸들 */,
쿠키 /* 불투명 포인터 */,
&cmdp, /* 명령 "list" */
1 /* 목록에 있는 항목 수 */);
/** 'errstatus' 확인 */
오류 상태 = lcb_wait(인스턴스); /*역주: 차단 */
lcb_wait 는 libcouchbase로 구현된 내부 이벤트 루프를 실행하고 최종적으로 결과 콜백을 호출합니다:
실제 결과는 libcouchbase에서 반환 값으로 전달되지 않고 콜백으로 전달됩니다. 콜백을 사용하는 이면의 아이디어는 결과 구조에 네트워크 버퍼에 대한 포인터가 포함되어 있다는 것입니다. 라이브러리가 실제로 결과를 '반환'한다면, 라이브러리가 임시 네트워크 버퍼를 할당된 메모리에 복사하고 사용자가 이를 해제해야 하므로 성능에 영향을 미칠 수 있습니다. 결과 콜백은 다음과 같습니다:
(명확성을 위해 상수 및 캐스팅은 생략).
특정 작업에 대해 호출할 콜백을 설치하려면 C libcouchbase 라이브러리 사용자가 설치해야 합니다. 이러한 콜백은 일반적인 응답 정보(예: 응답, 오류 상태 등)와 함께 작업별 불투명 포인터( '쿠키') 사용자가 전달한 포인터(이 경우 C 확장자)입니다. 이 포인터는 사용 제어 및 관리되며 일반적으로 사용자가 응답을 주어진 요청과 연관시키는 데 사용할 수 있는 컨텍스트/애플리케이션별 데이터를 보유합니다.
libcouchbase 및 CPython과 상호 작용하기
확장 프로그램에 의해 노출되는 공개 API의 관점에서 볼 때 각 작업은 '결과' 인스턴스(또는 그 적절한 하위 클래스)가 사용자에게 반환됩니다. '결과' 객체에는 작업 결과 및 관련 메타데이터에 대한 정보가 포함됩니다. C 라이브러리와 상호 작용할 때 Result 객체는 다음을 상속하는 확장 클래스입니다. PyObject 를 생성하고 각 요청 전에 C 라이브러리에 할당합니다. 그런 다음 '쿠키' 인수를 예약된 작업에 추가합니다. C 라이브러리가 설치된 콜백을 호출하면, 콜백은 예약된 작업의 결과 객체에 관련 정보를 추가합니다. 마지막으로 이벤트 루프가 종료되면 결과 객체가 사용자에게 전달됩니다.
간단한 구현은 다음과 같습니다:
Connection_get(pycbc_Connection *self, PyObject *args)
{
const char *키;
만약 (!PyArg_ParseTuple("s", &키, args) {
반환 NULL;
}
lcb_get_cmd_t 명령어, *commandp = &명령;
명령->v.v0.키 = 키;
명령->v.v0.nkey = strlen(키);
pycbc_ValueResult *결과 = PyObject_CallObject(&pycbc_ValueResultType, NULL);
lcb_get(self->인스턴스, 결과, &commandp, 1);
lcb_wait(self->인스턴스);
반환 결과;
}
lcb_wait()은 결과가 도착하고 해당 콜백이 호출될 때까지 차단됩니다. 콜백은 다음과 같이 표시됩니다:
{
pycbc_ValueResult *결과 = (pycbc_ValueResult *)쿠키;
결과->값 = PyString_FromStringAndSize(resp->v.v0.바이트, resp->v.v0.nbytes);
결과->rc = err;
결과->플래그 = resp->v.v0.플래그;
결과->cas = resp->v.v0.cas;
}
SDK는 트랜스코더 및 다양한 값 형식 등을 지원하기 때문에 키와 값을 변환하는 실제 코드는 이보다 훨씬 더 복잡하지만 여기서 논의하는 실제 아키텍처와는 관련이 없습니다 [ 참고 ].
좀 더 구체적으로 설명하자면 결과 객체는 콜백 내에서만 할당되며, 대기 시퀀스 전에 실제로 할당되는 것은 멀티 결과 객체인 dict 서브클래스. 이 멀티 결과 객체는 몇 가지 특정 내부 매개변수를 추가하여 딕셔너리를 확장합니다; 대기 시퀀스 중에 호출되는 각 콜백에 대해 새로운 결과 객체가 할당되고 여기에 삽입됩니다. 멀티 결과 딕셔너리에서 액세스하려는 데이터 항목의 키를 키로 지정합니다. 따라서 코드는 실제로 다음과 같이 보입니다:
pycbc_MultiResult *mres = PyObject_CallObject(&pycbc_MultiResultType, NULL);
lcb_get(self->인스턴스, mres, &commandp, 1);
/* … */
및 콜백에서
pycbc_MultiResult *mres = (pycbc_MultiResult *)쿠키;
pycbc_ValueResult *vres = PyObject_CallObject(&pycbc_ValueResultType, NULL);
/* ... vres의 멤버 할당 */
PyObject *keyobj = PyString_FromStringAndSize(resp->v.v0.키, resp->v.v0.nkey);
vres->키 = keyobj;
PyDict_SetItem(&mres->dict, 키, vres);
Py_DECREF(vres);
/* … */
이 내부 설계는 복잡하지만 매우 효율적인 코드 재사용(따라서 테스트 및 디버깅)을 가능하게 하며, 성능도 매우 뛰어납니다. 또한 이러한 추상화는 전적으로 순수 C로 구현되어 오버헤드가 최소화됩니다.
비동기 I/O 구현하기
이벤트 루프 통합
앞서 언급한 C 라이브러리는 확장 가능한 IO 플러그인 인터페이스를 제공합니다. 앞서 C 라이브러리가 자체 내부 이벤트 루프를 사용한다고 언급했지만 이는 전적으로 사실이 아니며, 오히려 소켓 감시자나 타임아웃 이벤트와 같은 일반적인 비동기 프리미티브 스케줄링을 위한 자체 함수를 구현할 수 있는 I/O 플러그인 API를 노출하는 것이 라이브러리가 하는 일입니다. 라이브러리 자체는 완전히 비동기식이기 때문에 비차단 프로그램 흐름과 통합할 수 있습니다.
이 API는 여기에서 확인할 수 있습니다: https://github.com/couchbase/libcouchbase/blob/62300167f6f7d0f84ee6ac2162591805dfbf163d/include/libcouchbase/types.h#L196-221
따라서 Twisted의 리액터 이벤트 루프와 통합하기 위해서는 Twisted의 리액터 메서드를 사용하여 일반적인 비동기 프리미티브를 구현하는 'IO 플러그인'을 작성해야 했는데, 그 결과는 여기에서 볼 수 있으며 그 자체로는 상당히 간단하다고 생각합니다: https://github.com/couchbaselabs/couchbase-twisted-client/blob/twisted/txcouchbase/iops.py
'is_sync' 프로퍼티는 Twisted의 리액터를 이벤트 루프 백엔드로 사용하여 동기 패턴의 기본 기능을 테스트하기 위한 것일 뿐입니다. 기본값이 아닙니다].
이 IO 플러그인 인터페이스를 파이썬에 노출하기 위해 이러한 프리미티브에 대한 파이썬 래퍼 클래스를 만들었습니다. IOEvent, 타이머 이벤트등입니다. 기본 개념은 이러한 객체에는 C 콜백 데이터에 대한 내부 포인터가 포함되어 있다는 것입니다. 또한 'IOPS' 클래스에서 이러한 객체를 하나 이상 관리합니다. 기본 개념은 C 라이브러리가 (확장자를 통해) 'IOPS' 객체에 이벤트 객체 중 하나를 전달하여 일종의 스케줄링 수정(예: 감시, 감시 해제, 삭제 등)을 요청합니다. 이벤트의 IOPS 객체는 실제 이벤트 루프 구현(이 경우 리액터)을 호출하여 원하는 이벤트를 예약하고 관련 이벤트 객체를 전달합니다. 이벤트가 준비되면 이벤트의 'ready_*' 메서드가 호출되면 C로 호출됩니다. 당연히 이 모든 것이 약간의 성능 저하를 유발하지만, 이제 코드가 모든 Python 이벤트 루프에서 비동기적으로 상호 작용할 수 있다는 이점이 있습니다.
이 모든 것을 종합하면 다음과 같습니다:
libcouchbase가 처음 소켓을 생성하면, 포인터로 노출되는 일종의 IO 이벤트 객체와 연결합니다:
이것은 다음과 같은 Python 코드를 호출합니다:
create_event(LCB_IO_OPT_T *iobase)
{
pycbc_iops_t = (pycbc_iops_t *)iobase;
PyObject *event_factory = PyObject_GetAttrString(io->py_impl, "create_event");
반환 PyObject_CallObject(event_factory, NULL);
}
이 'event_factory'는 몇 가지 필드가 추가된 IOEvent의 서브클래스를 반환해야 하며, 다음과 같이 보일 수 있습니다.
이렇게요:
클래스 MyIOEvent(IOEvent):
def doRead(self):
self.ready_r()
def doWrite(self):
self.ready_w()
클래스 IOPS(객체):
def create_event(self):
반환 MyIOEvent()
이제 라이브러리에서 특정 소켓을 사용할 수 있을 때 알림을 받으려면 다음과 같은 작업을 수행합니다:
그러면 이걸 호출합니다:
업데이트_이벤트(LCB_IO_OPT_T IOBASE, void *이벤트, int sockfd, 짧은 플래그, void (*콜백)(int, 짧은, void *), void *데이터)
{
pycbc_io_opt_t *io = (pycbc_io_opt_t *)iobase;
pycbc_IOEvent *ev = (pycbc_IOEvent *)이벤트;
이벤트->fd = sockfd; /* '.fileno()'를 위한 저장소 */
이벤트->callback_info.콜백 = 콜백;
이벤트->callback_info.데이터 = 데이터;
PyObject *args = Py_BuildValue("(O,I)", ev, 플래그);
PyObject *meth = PyObject_GetAttrString(io->py_impl, "update_event");
PyObject_CallObject(meth, args);
}
파이썬에서는 이렇게 보일 수 있습니다:
만약 플래그 및 READ_EVENT:
self.reactor.addReader(이벤트)
'event' 매개변수는 이전 'create_event' 메서드가 반환하는 객체로, doRead에 필요한 구현이 포함된 MyIOEvent의 인스턴스를 반환합니다.
나중에 어느 시점에 리액터는 기본 fileno()를 읽을 수 있는 것을 감지하고 위에 표시된 'doRead()' 메서드를 호출합니다. 우리 구현에서 doRead는 C로 구현된 메서드인 'ready_r()'을 호출합니다:
Event_ready_r(pycbc_IOEvent *이벤트)
{
이벤트->callback_info.콜백(이벤트->fd, READ_EVENT, 이벤트->callback_info.데이터);
}
향후/지연 연결 API
실제 데이터 액세스 API를 비동기식으로 만들기 위해 연결 객체에 '비동기'로 표시하는 몇 가지 비공개 매개 변수를 추가했습니다. 기본적으로 연결 객체 내부에 필드를 설정하고 적절한 'IOPS' 인스턴스입니다. 각 작업이 예약되면 확장 프로그램은 연결 개체의 'F_ASYNC' 플래그가 설정되어 있는지 확인합니다. 설정되어 있으면 lcb_wait()를 반환하지만 AsyncResult 객체( 멀티 결과)를 통해 결과를 기다리지 않고 바로 확인할 수 있습니다. 이 'AsyncResult' 객체에 'errback' 및 '콜백' 프로퍼티는 결과가 준비되면 호출됩니다.
마찬가지로 콜백 코드에서 연결 'F_ASYNC' 플래그가 설정되어 있으면 결과가 수신될 때마다 성공 또는 실패에 따라 관련 AsyncResult.callback 또는 AsyncResult.errback 함수가 호출됩니다.
내부 구조의 가장 큰 장점은 비동기 작동을 허용하기 위해 수정이 거의 필요하지 않으므로 동기화 API의 모든 안정성이 새로운 비동기 인터페이스에 추가된다는 점입니다.
간단한 'get' 구현은 이제 C에서 다음과 같이 보입니다:
get(pycbc_Connection *self, PyObject *args)
{
/* 정상적으로 인수 유효성 검사 수행 */
…
pycbc_MultiResult *mres;
만약 (self->플래그 & F_ASYNC) {
mres = PyObject_CallObject(&pycbc_AsyncResultType, NULL);
} else {
mres = PyObject_CallObject(&pycbc_MultiResultType, NULL);
}
/** 인수의 유효성 검사 등을 수행합니다. */
…
err = lcb_get(self->인스턴스, mres, &commandp, 1);
만약 (self->플래그 & F_ASYNC) {
lcb_wait(self->인스턴스);
}
반환 mres;
}
콜백에서도 마찬가지입니다;
get_callback(lcb_t 인스턴스, const void *쿠키, LCB_ERROR_t 오류, const LCB_GET_REP_T *resp)
{
pycbc_MultiResult *mres = 쿠키;
pycbc_ValueResult *vres = PyObject_CallObject(&pycbc_ValueResultType, NULL);
/** 값 결과를 설정하고 사전에 삽입합니다 ... */.
…
만약 (mres->부모->플래그 & F_ASYNC) {
/*역주: 서브클래스인 AsyncResult입니다 */.
PyObject_CallObject( ((pycbc_AsyncResult *)mres)->콜백, NULL);
Py_DECREF(mres); 반환하지 않습니다 /*역주: 반환하지 않습니다 */.
}
}
새로운 'txcouchbase' 패키지가 추가되었으며, 여기에는 자체 연결 클래스입니다. 이 연결 클래스는 이러한 내부 플래그를 설정합니다. 또한 각 연산에 대해 반환되는 AsyncResult 객체는 다음 구조에 해당하는 콜백으로 래핑됩니다:
async_res = super(연결, self).get(*args, **kwargs)
d = 지연()
async_res.콜백 = d.콜백
async_res.errback = d.errback
반환 d
결과가 준비되면 콜백이 호출됩니다. 멀티 결과 또는 결과 객체를 반환합니다(API에서 단일 또는 다중 변형 작업이 수행되었는지 여부에 따라 다름).
정말 멋지네요, 마크. 저희 팀에서는 카우치베이스와 트위스트(오토반과 함께)를 광범위하게 사용하고 있으며, 앞으로 몇 주 안에 사용해보고 피드백을 제공하겠습니다.
수고하셨습니다!