실시간 뷰 구성요소 빌드 및 배포
GitLab은 사용자 입력을 받아들이고 상태 변경을 사용자에게 다시 표시하는 개별 뷰 구성요소를 통해 대화형 사용자 경험을 제공합니다. 예를 들어, 병합 요청 페이지에서 사용자는 승인하거나 코멘트를 남기거나 CI/CD 파이프라인과 상호 작용하는 등의 작업을 수행할 수 있습니다.
그러나 GitLab은 종종 상태 업데이트를 즉시 반영하지 않습니다. 이는 페이지의 일부가 사용자가 페이지를 새로 고침한 후에만 업데이트되는 구식 데이터를 표시하는 것을 의미합니다.
이를 해결하기 위해 GitLab은 뷰 구성요소가 웹소켓을 통해 실시간으로 상태 업데이트를 받을 수 있도록 하는 기술과 프로그래밍 API를 도입했습니다.
다음 설명서에서는 GitLab Ruby on Rails 서버에서 실시간으로 업데이트를 받는 뷰 구성요소를 빌드하고 배포하는 방법을 안내합니다.
참고:
Action Cable 및 GraphQL 구독은 진행 중인 작업이며 활발히 개발 중입니다. 개발자들은 자신들의 사용 사례를 평가하여 이 도구들이 적합한 도구인지 확인해야 합니다. 확신이 없다면 #f_real-time
내부 Slack 채널에서 도움을 요청하세요.
실시간 뷰 구성요소 빌드
필수 구성 요소:
다음을 참조하십시오:
GitLab에서 실시간 뷰 구성요소를 빌드하려면 다음을 수행해야 합니다:
- GitLab 프론트엔드에 Apollo 구독을 통합한 Vue 구성요소를 통합합니다.
- GitLab Ruby on Rails 백엔드에서 GraphQL 구독을 추가하고 트리거합니다.
Vue 구성요소에 Apollo 구독 통합
참고: 현재의 실시간 스택은 클라이언트 코드를 Vue를 렌더링 레이어로 사용하고 Apollo를 상태 및 네트워킹 레이어로 사용한다고 가정합니다. 아직 Vue + Apollo로 마이그레이션되지 않은 GitLab 프론트엔드의 일부를 다루고 있다면 먼저 해당 작업을 완료해야 합니다.
가상의 IssueView
Vue 구성요소를 고려해보십시오. 이는 GitLab Issue
데이터를 관찰하고 렌더링하는 것으로 가정합니다. 여기서는 간단히 이 구성요소가 문제의 제목과 설명을 렌더링하는 것으로 가정합니다.
import issueQuery from '~/issues/queries/issue_view.query.graqhql';
export default {
props: {
issueId: {
type: Number,
required: false,
default: null,
},
},
apollo: {
issue: {
// 초기 가져오기에 사용되는 쿼리.
query: issueQuery,
// 초기 가져오기 쿼리에 사용되는 인수 바인딩.
variables() {
return {
iid: this.issueId,
};
},
// 응답 데이터를 뷰 속성에 매핑.
update(data) {
return data.project?.issue || {};
},
},
},
// 반응형 Vue 구성요소 데이터. Apollo는 쿼리 반환 또는 구독 화이어될 때 이러한 데이터를 업데이트합니다.
data() {
return {
issue: {}, // 뷰가 로드되는 동안 초기 상태를 반환하는 것이 좋습니다.
};
},
};
쿼리는 다음과 같아야 합니다:
-
app/assets/javascripts/issues/queries/issue_view.query.graqhql
에 정의되어 있어야 합니다. -
다음 GraphQL 작업을 포함해야 합니다:
query gitlabIssue($iid: String!) { # 여기서 경로를 하드 코딩하여 설명하고 있습니다. 실전에서는 하지 마십시오. project(fullPath: "gitlab-org/gitlab") { issue(iid: $iid) { title description } } }
지금까지 이 뷰 구성요소는 데이터로 자신을 채우기 위한 초기 가져오기 쿼리만 정의했습니다. 이는 뷰에 의해 시작되는 HTTP POST 요청으로 보내진 일반적인 GraphQL query
작업입니다. 서버에서의 이후 업데이트는 이 뷰를 구식으로 만듭니다. 이것이 서버에서 업데이트를 받기 위해서는 다음을 수행해야 합니다:
- GraphQL 구독 정의 추가.
- Apollo 구독 훅 정의.
GraphQL 구독 정의 추가
구독은 GraphQL 쿼리를 정의하지만, 이것은 GraphQL subscription
작업 내부에 포장되어 있습니다. 이 쿼리는 백엔드에 의해 시작되며 그 결과는 웹소켓을 통해 뷰 구성요소로 푸시됩니다.
초기 가져오기 쿼리와 유사하게 다음을 수행해야 합니다:
-
app/assets/javascripts/issues/queries/issue_updated.subscription.graqhql
에 구독 파일을 정의해야 합니다. -
파일에 다음과 같은 GraphQL 작업을 포함해야 합니다:
subscription issueUpdatedSubscription($iid: String!) { issueUpdated($issueId: IssueID!) { issue(issueId: $issueId) { title description } }
변경사항을 받을 때는 다음과 같은 명명규칙을 사용해야 합니다:
- 구독의 작업 이름을
SubscriptionEE
를 사용하여 끝내야 합니다. GitLab EE에 독점적인 경우에만 사용합니다. 예:issueUpdatedSubscription
, 또는issueUpdatedSubscriptionEE
. - 구독 이벤트의 이름에 “발생했음” 동사를 사용해야 합니다. 예:
issueUpdated
.
구독 정의는 일반적인 쿼리와 유사해 보이지만 중요한 이해가 필요한 몇 가지 주요 차이점이 있습니다:
-
query
:- 프론트엔드에서 시작됩니다.
- URL에서 엔티티가 일반적으로 참조되는 방식과 일치하는 내부 ID(
iid
, 숫자)가 사용됩니다. 이 내부 ID는 일반적으로 URL에 의해 참조되는 것으로, 경로에 중첩되어 있으므로fullPath
아래에 쿼리를 중첩해야 합니다.
-
subscription
:- 미래의 업데이트를 받기 위해 백엔드에 대한 프론트엔드의 요청입니다.
- 다음 사항으로 구성됩니다:
- 구독 자체를 설명하는 작업 이름(
issueUpdatedSubscription
이 이 예에서). - 중첩된 이벤트 쿼리(이 예에서
issueUpdated
). 중첩된 이벤트 쿼리:- 동일한 이름의 GraphQL 트리거 실행 시 실행되므로, 구독에 사용된 이벤트 이름은 백엔드에서 사용된 트리거 필드와 일치해야 합니다.
- GraphQL에서 리소스를 식별하는 선호되는 방법인 숫자 대신 전역 ID 문자열을 사용합니다. 자세한 내용은 GraphQL 전역 ID를 참조하십시오.
- 구독 자체를 설명하는 작업 이름(
Apollo 구독 훅 정의
구독을 정의한 후, Apollo의 subscribeToMore
속성을 사용하여 뷰 구성요소에 추가합니다.
import issueQuery from '~/issues/queries/issue_view.query.graqhql';
import issueUpdatedSubscription from '~/issues/queries/issue_updated.subscription.graqhql';
export default {
// 이전과 마찬가지로...
// ...
apollo: {
issue: {
// 이전과 마찬가지로...
// ...
// 이 Apollo 훅은 실시간 푸시를 가능하게 합니다.
subscribeToMore: {
// 미래의 업데이트를 반환하는 구독 작업.
document: issueUpdatedSubscription,
// 구독 작업에 사용되는 인수 바인딩.
variables() {
return {
iid: this.issueId,
};
},
// 기능 플래그를 사용할 때 유용한 true | false를 반환합니다.
skip() {
return this.shouldSkipRealTimeUpdates;
},
},
},
},
// 이전과 마찬가지로...
// ...
computed: {
shouldSkipRealTimeUpdates() {
return false; // 여기서 기능 플래그를 확인할 수 있을 것입니다.
},
},
};
이제 Apollo를 통해 뷰 구성요소가 웹소켓 연결을 통해 업데이트를 받을 수 있도록 할 수 있습니다. 다음으로 서버에서 이벤트가 트리거되어 프론트엔드로 푸시 업데이트를 시작하는 방법을 다룹니다.
GraphQL 구독 트리거
WebSocket에서 업데이트를 수신할 수 있는 보기 컴포넌트를 작성하는 것은 반의 한 과정에 불과합니다. GitLab Rails 애플리케이션에서 우리는 다음 단계를 수행해야 합니다.
-
GraphQL::Schema::Subscription
클래스를 구현합니다. 이 클래스:-
graphql-ruby
에서 프론트엔드에서 보낸subscription
연산을 해결하는 데 사용됩니다. - 구독이 가져올 수 있는 인수와 호출자에게 반환되는 payload을 정의합니다.
- 호출자가 이 구독을 생성할 권한이 있는지 확인하기 위해 필요한 비즈니스 로직을 실행합니다.
-
-
Types::SubscriptionType
클래스에 새로운field
를 추가합니다. 이 field는 Vue 컴포넌트를 통합할 때 사용되는 이벤트 이름을GraphQL::Schema::Subscription
클래스에 매핑합니다. - 이벤트 이름과 일치하는 메서드를
GraphqlTriggers
에 추가하여 해당 GraphQL 트리거를 실행합니다. - 영역 내 로직의 일환으로 새 트리거를 실행하려면 서비스 또는 Active Record 모델 클래스를 사용합니다.
구독 구현하기
이미 GraphQL::Schema::Subscription
로 구현된 이벤트에 구독하려면 이 단계는 선택 사항입니다.
그렇지 않은 경우, 새로운 구독을 구현하는 app/graphql/subscriptions/
에 새 클래스를 만듭니다.
예를 들어 Issue
가 업데이트되면 발생하는 issueUpdated
이벤트의 경우, 구독 구현은 다음과 같습니다:
module Subscriptions
class IssueUpdated < BaseSubscription
include Gitlab::Graphql::Laziness
payload_type Types::IssueType
argument :issue_id, Types::GlobalIDType[Issue],
required: true,
description: '이슈의 ID.'
def authorized?(issue_id:)
issue = force(GitlabSchema.find_by_gid(issue_id))
unauthorized! unless issue && Ability.allowed?(current_user, :read_issue, issue)
true
end
end
end
이 새 클래스를 만들 때:
- 모든 구독 유형이 Subscriptions::BaseSubscription
에서 상속되는지 확인합니다.
- payload_type
을 사용하여 구독된 쿼리가 액세스할 수 있는 데이터를 나타내거나 노출하려는 개별 field
를 정의합니다.
- 각 클라이언트가 구독할 때 또는 이벤트가 발생할 때 호출되는 subscribe
및 update
훅을 정의할 수도 있습니다. 이러한 메서드를 사용하는 방법은 공식 문서를 참조합니다.
- 필요한 권한 확인을 수행하기 위해 authorized?
를 구현합니다. 이러한 확인은 subscribe
또는 update
모두 호출될 때마다 실행됩니다.
GraphQL 구독 클래스에 대해 자세히 알아보려면 공식 문서를 참조합니다.
구독 연결
새로운 구독 클래스를 구현한 경우이 단계는 건너뜁니다.
새로운 구독 클래스를 구현하면 그 클래스를 field
에 매핑하여 SubscriptionType
에서 실행할 수 있도록 해야 합니다. Types::SubscriptionType
클래스를 열고 새로운 field를 추가합니다:
module Types
class SubscriptionType < ::Types::BaseObject
graphql_name 'Subscription'
# 기존 필드
# ...
field :issue_updated,
subscription: Subscriptions::IssueUpdated, null: true,
description: '이슈가 업데이트될 때 트리거됩니다.'
end
end
참고:
EE(엔터프라이즈 에디션) 구독을 연결하는 경우 EE::Types::SubscriptionType
을 업데이트합니다.
issue_updated
인수가 프론트엔드에서 보내는 subscription
요청의 이름과 일치하는지 확인합니다. 그렇지 않으면 graphql-ruby
는 어떤 구독자에게 알릴지 모릅니다. 이제 해당 이벤트가 트리거될 수 있습니다.
새로운 트리거 추가
기존 트리거를 재사용할 수 있는 경우이 단계를 건너뜁니다.
이벤트를 트리거하는 것을 보다 간단하게 만들기 위해 GitlabSchema.subscriptions.trigger
주변에 퍼사드를 사용합니다.
GraphqlTriggers
에 새 트리거를 추가합니다:
module GraphqlTriggers
# 기존 트리거
# ...
def self.issue_updated(issue)
GitlabSchema.subscriptions.trigger(:issue_updated, { issue_id: issue.to_gid }, issue)
end
end
참고:
트리거가 EE(엔터프라이즈 에디션) 구독을 위한 것이라면 EE::GraphqlTriggers
을 업데이트합니다.
- 첫 번째 인수
:issue_updated
는 이전 단계에서 사용된field
이름과 일치해야 합니다. - 인수 해시는 이벤트를 게시할 주제를 식별하기 위해 GraphQL에서 사용합니다.
최종 단계는 이 트리거 함수를 호출하는 것입니다.
트리거 실행
이 단계의 구현은 구축 중인 내용에 따라 다릅니다. 예를 들어, 이슈의 필드가 변경되는 경우, Issues::UpdateService
를 확장하여 GraphqlTriggers.issue_updated
를 호출할 수 있습니다.
실시간 보기 컴포넌트가 이제 작동합니다. 이제 이슈의 업데이트가 GitLab UI로 즉시 전파되어야 합니다.
실시간 보기 컴포넌트 배포
WebSocket은 상대적으로 새로운 기술이며 대규모로 지원하는 것은 일부 도전을 유발합니다. 따라서 새로운 기능을 아래 지침을 사용하여 배포해야 합니다.
실시간 컴포넌트 배포
WebSocket을 통한 업데이트를 시뮬레이트하는 것은 백엔드 코드가 준비되지 않은 경우에는 구현하기 어려우므로 프론트엔드와 백엔드 작업을 동시에 수행할 수 있습니다.
그러나 변경 사항을 별도의 병합 요청으로 보내고 먼저 백엔드 변경 사항을 배포하는 것이 안전합니다. 이렇게 하면 프론트엔드가 이벤트를 구독하기 시작할 때 백엔드가 이미 준비되어 있습니다.
기존 WebSocket 연결 재사용
기존 연결을 재사용하는 기능은 최소한의 리스크를 수반합니다. 더 많은 제어를 셀프 호스팅 고객에게 제공하려면 기능 플래그 롤아웃이 권장됩니다. 그러나 GitLab.com에 대한 새로운 연결을 추정하거나 백분율로 배포할 필요는 없습니다.
새 WebSocket 연결 도입
GitLab 애플리케이션의 일부에 WebSocket 연결을 도입하는 모든 변경은 유지보수를 담당하는 노드 및 Redis 및 주요 데이터베이스와 같은 하위 서비스에 일부 확장 가능성 리스크를 수반합니다.
최대 연결 수 추정
GitLab.com에서 완전히 활성화된 최초의 실시간 기능은 실시간 담당자 할당였습니다. 이 이슈 페이지의 최대 처리량과 동시 WebSocket 연결의 봉쇄를 비교하여, 초당 1개 요청당 약 4200개의 WebSocket 연결이 추가된다고 추정할 수 있습니다.
새로운 기능이 어떤 영향을 미칠지 이해하기 위해 이 기존 봉쇄 (RPS)를 요청은
(n
) 페이지에서 추정하고, 다음에 적용하는 공식은 다음과 같습니다.
(n * 4200) / peak_active_connections
현재 활성 연결은 이 Grafana 차트 에서 확인할 수 있습니다.
이 계산은 근사적인 것이며 새로운 기능이 배포됨에 따라 재검토되어야 합니다. 기존 용량의 일부로 지원해야 하는 용량의 대략적인 추정치를 제공합니다.
Graduated roll-out
새로운 용량은 현재 포화 상태 및 필요한 새 연결의 비율에 따라 지원되어야 할 수 있습니다. 대부분의 경우 Kubernetes를 사용하면 이 과정이 비교적 간단하지만, 하향 스트림 서비스에는 여전히 리스크가 남아 있습니다.
이를 완화하기 위해 새로운 WebSocket 연결을 설정하는 코드가 특성 플래그 처리되고 off
로 기본 설정되도록합니다. 특성 플래그의 백분율 기반 점진적인 배포는 영향을 관찰할 수 있도록 합니다.
- WebSocket 대시보드에서 효과를 관찰할 수 있도록 특성 플래그 배포
- 특성 플래그 배포 이슈를 생성합니다.
- 예상되는 동작 섹션에 필요한 새 연결을 추가합니다.
- Plan 및 Scalability팀의 구성원을 복사하여 백분율 기반 배포 계획을 추정합니다.
Backward compatibility
특성 플래그 배포 기간 동안과 그 후에는 실시간 기능이 하위 호환되어야 하거나 적어도 점진적으로 떨어져야 합니다. 모든 고객이 Action Cable을 활성화하지 않았으며 Action Cable을 기본으로 활성화하기 전에 추가 작업이 필요합니다.
실시간을 요구하는 것은 대전환 변경을 나타내므로, 다음에이 변경사항을 적용할 수 있는 기회는 버전 15.0입니다.
GitLab.com의 실시간 인프라
GitLab.com에서 WebSocket 연결은 Kubernetes로 배포된 일반 웹 플릿과 완전히 별도의 인프라에서 제공됩니다. 이로써 요청을 처리하는 노드에 대한 리스크는 제한되지만 공유 서비스에는 그렇지 않을 수 있습니다. WebSockets Kubernetes 배포에 대한 자세한 내용은 이 이픽을 참조하십시오.
GitLab 실시간 스택의 심도 있는 기능
서버에서 시작된 푸시가 네트워크 전파 및 사용자 상호 작용없이 클라이언트에서 업데이트를 트리거해야하기 때문에 실시간 기능은 프론트엔드 및 백엔드를 포함한 전체 스택을 살펴봄으로써만 이해할 수 있습니다.
참고:
역사적인 이유로 클라이언트가 변경 사항을 폴링하는 대기 상태에서 업데이트를 서비스하는 컨트롤러 라우트는 realtime_changes
라고합니다. 조건부 GET 요청을 사용하며이 가이드에서 다루는 실시간 동작과는 관련이 없습니다.
클라이언트로 푸시 된 모든 실시간 업데이트는 GitLab Rails 애플리케이션에서 기원합니다. 우리는 다음 기술을 사용하여이러한 업데이트를 시작하고 처리합니다.
GitLab Rails 백엔드에서:
- Redis PubSub으로 구독 상태를 처리합니다.
- WebSocket 연결 및 데이터 전송을 처리하는 Action Cable.
- GraphQL 구독 및 트리거를 구현하기 위해 graphql-ruby
를 사용합니다.
GitLab 프론트엔드에서: - GraphQL 요청, 라우팅 및 캐싱을 처리하기 위해 Apollo Client를 사용합니다. - 실시간으로 업데이트되는 보기 구성 요소를 정의하고 렌더링하기 위해 Vue.js를 사용합니다.
다음 그림은이러한 층 간의 데이터 전파 방법을 설명합니다.
GraphQL 구독: Backend
GitLab은 클라이언트가 GraphQL 쿼리를 사용하여 서버로부터 구조화된 데이터를 요청할 수 있도록 지원합니다. GraphQL은 일반적으로 표준 요청-응답 주기를 따르는 클라이언트 주도의 HTTP POST 요청입니다. 실시간 기능을 위해 우리는 대신 GraphQL 구독을 사용합니다. 이는 발행/구독 패턴의 구현입니다. 이 접근에서 클라이언트는 먼저 GraphqlChannel
에 구독 요청을 보내고 다음과 같은 정보를 포함합니다:
- 구독
field
의 이름(이벤트 이름). - 이 이벤트가 트리거될 때 실행할 GraphQL 쿼리.
이 정보는 서버에서 이 이벤트 스트림을 나타내는 topic
을 생성하는 데 사용됩니다. 이 topic은 구독 인수와 이벤트 이름에서 파생된 고유한 이름이며, 이 이벤트가 트리거되면 알림을 받아야 하는 모든 구독자를 식별하는 데 사용됩니다. 같은 topic에 여러 클라이언트가 구독할 수 있습니다. 예를 들어 issuableAssigneesUpdated:issuableId:<hashed_id>
은 주어진 ID의 이슈를 대상으로 할당 받는 사람이 변경될 때 업데이트를 원하는 경우 구독하는 클라이언트에게 서비스할 수 있습니다.
백엔드는 “에픽에 이슈 추가됨” 또는 “사용자가 이슈에 할당됨”과 같은 도메인 이벤트에 대한 응답으로 일반적으로 구독을 트리거합니다. GitLab에서는 서비스 객체 또는 ActiveRecord 모델 객체가 될 수 있습니다. 트리거는 GitlabSchema.subscriptions.trigger
를 호출하여 해당 이벤트 이름과 인수로 실행되며, 여기서 graphql-ruby
는 topic을 파생시킵니다. 그런 다음 이 topic에 대한 모든 구독자를 찾아 각 구독자의 쿼리를 실행하고 결과를 모든 topic 구독자에게 다시 전달합니다.
우리는 GraphQL 구독의 기본 전송으로서 Action Cable을 사용하기 때문에, 각 구독자에 대해 두 개의 PubSub 채널이 사용됩니다:
- 각 topic당 하나의
graphql-event:<namespace>:<topic>
채널. 이 채널은 모든 잠재적인 클라이언트 사이에서 공유되며, 어떤 클라이언트가 어떤 이벤트에 구독되어 있는지 추적하는 데 사용됩니다.namespace
의 사용은 선택적이며, 빈 칸일 수 있습니다. - 각 클라이언트당 하나의
graphql-subscription:<subscription-id>
채널. 이 채널은 해당 클라이언트에게 쿼리 결과를 전송하는 데 사용되므로 서로 다른 클라이언트 간에 공유할 수 없습니다.
다음 섹션에서는 GitLab 프론트엔드가 실시간 업데이트를 구현하기 위해 GraphQL 구독을 어떻게 사용하는지 설명합니다.
GraphQL 구독: Frontend
GitLab 프론트엔드는 루비가 아닌 JavaScript를 실행하는데, 클라이언트에서 서버로 GraphQL 쿼리, 뮤테이션 및 구독을 전송하기 위한 다른 GraphQL 구현이 필요합니다. 우리는 이를 위해 Apollo를 사용합니다.
Apollo는 JavaScript에서의 GraphQL의 효율적인 구현이며, apollo-server
와 apollo-client
로 나뉘어 있습니다. 또한 추가적인 유틸리티 모듈을 제공합니다. 우리는 루비 백엔드를 실행하기 때문에 apollo-server
대신 apollo-client
를 사용합니다.
Apollo는 다음을 단순화합니다:
- 네트워킹, 연결 관리 및 요청 라우팅.
- 클라이언트 측 상태 관리 및 응답 캐싱.
- 뷰 컴포넌트를 사용하여 GraphQL을 통합하는 것.
참고: Apollo Client 문서를 읽을 때는 React.js가 뷰 렌더링에 사용된다고 가정합니다. GitLab에서는 React.js를 사용하지 않습니다. 우리는 Apollo를 통해 Vue.js를 사용하고 있으며, Vue.js 어댑터를 사용하여 Apollo와 통합합니다.
Apollo는 다음과 같이 정의된 함수와 후크를 제공합니다:
- 뷰가 쿼리, 뮤테이션 또는 구독을 보내는 방법.
- 응답을 처리해야 하는지 여부.
- 응답 데이터를 캐싱하는지 여부.
진입점은 페이지 내 모든 뷰 컴포넌트 간에 공유되는 GraphQL 클라이언트 객체인 ApolloClient
입니다. 모든 뷰 컴포넌트는 서버와 통신하기 위해 내부적으로 사용합니다.
다른 유형의 요청을 어떻게 라우팅해야 하는지 결정하기 위해 Apollo는 ApolloLink
추상화를 사용합니다. 구체적으로, ActionCableLink
를 사용하여 실제 서버 구독을 다른 GraphQL 요청과 분리합니다. 이렇게 함으로써:
- Action Cable에 대한 WebSocket 연결을 설정합니다.
- 서버 푸시를 클라이언트의
Observable
이벤트 스트림에 매핑하여 뷰가 스스로를 업데이트하는 데 구독할 수 있도록 합니다.
Apollo 및 Vue.js에 대한 자세한 정보는 GitLab GraphQL 개발 가이드를 참조하세요.