vitess 분석: vtgate, mysql proxy server를 여는 과정
Vitess는 MySQL을 기반으로 구축된 클라우드 네이티브 수평 확장형 분산 데이터베이스 시스템입니다. 이번 “vitess 분석” 시리즈에서는 Vitess의 동작 원리를 전반적으로 살펴봅니다. 가장 먼저 볼 부분은 서버를 열고, Vitess가 클라이언트 커넥션을 대기하기 시작하는 지점입니다. 이 역할은 Vitess 구성 요소 중 vtgate가 담당하며, vtgate는 클라이언트로부터 MySQL 쿼리를 받아 적절한 vttablet으로 전달하는 프록시 서버 역할을 합니다. 따라서 이번 글에서는 vtgate가 서버를 여는 과정부터 먼저 분석해보겠습니다.
이 글은 다음 문서를 따라 설치한 환경을 기준으로 진행합니다.
| [Vitess | Vitess Operator for Kubernetes](https://vitess.io/docs/21.0/get-started/operator/) |
해당 문서를 읽으려면 Vitess 아키텍처에 대한 기본 이해가 있으면 좋습니다.
| [Vitess | Architecture](https://vitess.io/docs/21.0/overview/architecture/) |
또한 Go의 net 패키지로 TCP 서버를 간단히 만들어본 경험이 있으면 이해가 수월합니다.
분석 대상 코드는 Vitess v21.0.3입니다.
GitHub - vitessio/vitess at 94fdc736eae8928a8fdde44e9ec9c3bee1868d6f
vtgate pod의 Vitess 진입점 분석
vtgate는 Vitess 내에서 트래픽을 vttablet 서버로 라우팅하고, 통합된 결과를 클라이언트에 다시 반환하는 프록시 서버입니다. MySQL 프로토콜을 지원하므로 클라이언트 입장에서는 MySQL 서버처럼 연결할 수 있습니다. Kubernetes에 Vitess Operator를 배포해보면 vtgate pod가 실행 중인 것을 확인할 수 있습니다. 아래는 vtgate Deployment를 확인한 결과입니다.
$ kubectl describe deploy example-zone1-vtgate-bc6cde92 -n vitess-operator
Name: example-zone1-vtgate-bc6cde92
Namespace: vitess-operator
...
Pod Template:
Labels: ...
Containers:
vtgate:
Image: vitess/lite:v21.0.3
Ports: 15000/TCP, 15999/TCP, 3306/TCP
...
Command:
/vt/bin/vtgate
Args:
--buffer_max_failover_duration=10s
--buffer_min_time_between_failovers=20s
--buffer_size=1000
--cell=zone1
--cells_to_watch=zone1
--enable_buffer=true
--grpc_max_message_size=67108864
--grpc_port=15999
--logtostderr=true
--mysql_auth_server_impl=static
--mysql_auth_server_static_file=/vt/secrets/vtgate-static-auth/users.json
--mysql_auth_static_reload_interval=30s
--mysql_server_port=3306
--port=15000
--service_map=grpc-vtgateservice
--tablet_types_to_wait=MASTER,REPLICA
--topo_global_root=/vitess/example/global
--topo_global_server_address=example-etcd-faf13de3-client.vitess-operator.svc:2379
--topo_implementation=etcd2
Limits:
memory: 256Mi
Requests:
cpu: 100m
memory: 256Mi
Liveness: http-get http://:web/debug/status delay=300s timeout=1s period=10s #success=1 #failure=30
Readiness: http-get http://:web/debug/health delay=0s timeout=1s period=10s #success=1 #failure=3
Environment: <none>
Mounts:
/vt/secrets/vtgate-static-auth from vtgate-static-auth-secret (ro)
Volumes:
vtgate-static-auth-secret:
Type: Secret (a volume populated by a Secret)
SecretName: example-cluster-config
Optional: false
여기서 확인할 수 있는 핵심 정보는 vtgate 실행을 위한 바이너리 호출 방식입니다. vtgate 바이너리는 /vt/bin/vtgate에 위치하며, Args에 보이는 값을 인자로 전달받습니다. 이 구조를 보면 vtgate가 Go의 Cobra를 통해 CLI 형식으로 호출될 것이라고 추측할 수 있습니다. 이 가정이 맞다면 진입점을 훨씬 쉽게 찾을 수 있습니다. Cobra는 기본적으로 help를 지원하므로, pod 내부에서 직접 확인해보면 됩니다.
$ kubectl exec -it example-zone1-vtgate-bc6cde92-8589757b5f-rfhjj -n vitess-operator -- /bin/sh
$ cd /vt/bin/
$ vtgate --help
VTGate is a stateless proxy responsible for accepting requests from applications and routing them to the appropriate tablet server(s) for query execution. It speaks both the MySQL Protocol and a gRPC protocol.
### Key Options
* `--srv_topo_cache_ttl`: There may be instances where you will need to increase the cached TTL from the default of 1 second to a higher number:
* You may want to increase this option if you see that your topo leader goes down and keeps your queries waiting for a few seconds.
다음과 같이 vtgate --help를 실행하면 사용법 안내가 출력됩니다. 이제 Vitess GitHub 저장소에서 VTGate is a stateless proxy responsible for accepting requests ... 문자열을 검색해보면 다음 위치를 찾을 수 있습니다.
// https://github.com/vitessio/vitess/blob/94fdc736eae8928a8fdde44e9ec9c3bee1868d6f/go/cmd/vtgate/cli/cli.go#L49
var (
// ...
Main = &cobra.Command{
Use: "vtgate",
Short: "VTGate is a stateless proxy responsible for accepting requests from applications and routing them to the appropriate tablet server(s) for query execution. It speaks both the MySQL Protocol and a gRPC protocol.",
Long: `VTGate is a stateless proxy responsible for accepting requests from applications and routing them to the appropriate tablet server(s) for query execution. It speaks both the MySQL Protocol and a gRPC protocol.
### Key Options
` +
"\n* `--srv_topo_cache_ttl`: There may be instances where you will need to increase the cached TTL from the default of 1 second to a higher number:\n" +
` * You may want to increase this option if you see that your topo leader goes down and keeps your queries waiting for a few seconds.`,
Example: `vtgate \
--topo_implementation etcd2 \
--topo_global_server_address localhost:2379 \
--topo_global_root /vitess/global \
--log_dir $VTDATAROOT/tmp \
--port 15001 \
--grpc_port 15991 \
--mysql_server_port 15306 \
--cell test \
--cells_to_watch test \
--tablet_types_to_wait PRIMARY,REPLICA \
--service_map 'grpc-vtgateservice' \
--pid_file $VTDATAROOT/tmp/vtgate.pid \
--mysql_auth_server_impl none`,
Args: cobra.NoArgs,
Version: servenv.AppVersion.String(),
PreRunE: servenv.CobraPreRunE,
RunE: run,
}
// ...
)
이 Main 커맨드는 다음 위치의 main()에서 실행됩니다.
// https://github.com/vitessio/vitess/blob/94fdc736eae8928a8fdde44e9ec9c3bee1868d6f/go/cmd/vtgate/vtgate.go#L24
func main() {
if err := cli.Main.Execute(); err != nil {
log.Exit(err)
}
}
Vitess 코드 내부: MySQL 프록시 서버 실행
지금까지는 Kubernetes 환경에서 Vitess를 띄운 뒤, 바이너리 실행 정보를 바탕으로 코드 진입점을 찾았습니다. 이제는 실제 코드 내부에서 어떤 초기화 로직이 호출되는지 따라가보겠습니다.
CLI 진입점을 찾았으니, 이제 Main 커맨드가 호출하는 run() 함수로 내려가보겠습니다. 이 함수에서는 토폴로지 서버를 열고 vtgate 초기화를 진행합니다.
// https://github.com/vitessio/vitess/blob/94fdc736eae8928a8fdde44e9ec9c3bee1868d6f/go/cmd/vtgate/cli/cli.go#L141
func run(cmd *cobra.Command, args []string) error {
defer exit.Recover()
// ✅ 환경변수 초기화
servenv.Init()
// ✅ topology server 초기화(etcd)
// Ensure we open the topo before we start the context, so that the
// defer that closes the topo runs after cancelling the context.
// This ensures that we've properly closed things like the watchers
// at that point.
ts := topo.Open()
defer ts.Close()
// topo.Server를 기반으로한 resilientServer 생성
resilientServer = srvtopo.NewResilientServer(ctx, ts, srvTopoCounts)
// ...
// 환경 변수 설정
env, err := vtenv.New(vtenv.Options{
// ...
})
// ...
// ✅ vtgate 초기화
// pass nil for HealthCheck and it will be created
vtg := vtgate.Init(ctx, env, nil, resilientServer, cell, tabletTypes, plannerVersion)
// ...
}
이 코드는 환경 변수 초기화, topology 서버 예를 들어 etcd 초기화, 그리고 vtgate 초기화를 담당합니다. 이 함수에서 가장 중요한 부분은 vtgate.Init 호출입니다. 첫 번째 읽기에서는 전체를 다 따라가기보다 ✅ 표시한 흐름만 확인해도 충분합니다.
// https://github.com/vitessio/vitess/blob/94fdc736eae8928a8fdde44e9ec9c3bee1868d6f/go/vt/vtgate/vtgate.go#L251
func Init(
ctx context.Context,
env *vtenv.Environment,
hc discovery.HealthCheck,
serv srvtopo.Server,
cell string,
tabletTypesToWait []topodatapb.TabletType,
pv plancontext.PlannerVersion,
) *VTGate {
// TabletGateway 생성
// vttablet 연결 관리 및 헬스 체크
gw := NewTabletGateway(ctx, hc, serv, cell)
gw.RegisterStats()
if err := gw.WaitForTablets(ctx, tabletTypesToWait); err != nil {
log.Fatalf("tabletGateway.WaitForTablets failed: %v", err)
}
// txConn 생성
// 트랜잭션 상태 관리
tc := NewTxConn(gw, getTxMode())
// 트랜잭션을 지원하는 샤딩 쿼리 실행기(?) 아직 뭔지 모름
// ScatterConn depends on TxConn to perform forced rollbacks.
sc := NewScatterConn("VttabletCall", tc, gw)
// 분산 트랜잭션
// TxResolver depends on TxConn to complete distributed transaction.
tr := txresolver.NewTxResolver(gw.hc.Subscribe(), tc)
srvResolver := srvtopo.NewResolver(serv, gw, cell)
// 키스페이스/타입/샤드 분해 및 라우팅
resolver := NewResolver(srvResolver, serv, cell, sc)
// binlog 스트리밍
vsm := newVStreamManager(srvResolver, serv, cell)
// 토폴로지 서버 초기화
ts, err := serv.GetTopoServer()
// 스키마 트래커 생성
var st *vtschema.Tracker
if enableSchemaChangeSignal {
st = vtschema.NewTracker(gw.hc.Subscribe(), enableViews, enableUdfs, env.Parser())
addKeyspacesToTracker(ctx, srvResolver, st, gw)
si = st
}
// executor 생성: 쿼리 실행 및 분석
executor := NewExecutor(
// ...
)
// ✅ vtgate 생성
vtgateInst := newVTGate(executor, resolver, vsm, tc, gw)
// ✅ 서버 실행
servenv.OnRun(func() {
// grpc/mysql 서버 등에 서비스 등록
for _, f := range RegisterVTGates {
f(vtgateInst)
}
// 스키마 트래커 시작
if st != nil && enableSchemaChangeSignal {
st.Start()
}
// 트랜잭션 리졸버 시작
tr.Start()
// ✅ mysql 서버 시작
srv := initMySQLProtocol(vtgateInst)
if srv != nil {
servenv.OnTermSync(srv.shutdownMysqlProtocolAndDrain)
servenv.OnClose(srv.rollbackAtShutdown)
}
})
// ...
return vtgateInst
}
✅ 표시만 기준으로 보면 이 함수는 서버 생성과 실행 흐름을 묶어둔 초기화 함수라고 볼 수 있습니다. 간단히 요약하면 다음과 같습니다.
vtgate인스턴스를 생성합니다.initMySQLProtocol을 호출해 MySQL 프로토콜 서버를 엽니다.
이제 서버를 실제로 여는 initMySQLProtocol에서 어떤 작업이 일어나는지 살펴보겠습니다. 이 함수는 vtgate가 MySQL 서버처럼 동작할 수 있도록 TCP 소켓 리스너를 초기화하는 함수입니다.
// https://github.com/vitessio/vitess/blob/94fdc736eae8928a8fdde44e9ec9c3bee1868d6f/go/vt/vtgate/plugin_mysql_server.go#L494
// initMySQLProtocol starts the mysql protocol.
// It should be called only once in a process.
func initMySQLProtocol(vtgate *VTGate) *mysqlServer {
// ...
// ✅ TCP 리스너 생성
// Create a Listener.
var err error
srv := &mysqlServer{}
srv.vtgateHandle = newVtgateHandler(vtgate) // ✅ 연결 핸들러 생성: 커넥션 이벤트 발생 시 핸들러 이벤트 호출
if mysqlServerPort >= 0 {
srv.tcpListener, err = mysql.NewListener( // ✅ TCP 서버 생성
// ...
)
// ...
// Start listening for tcp
// ✅ TCP 서버 실행
go srv.tcpListener.Accept()
}
// ...
}
이 함수는 그 밖에도 몇 가지 역할을 더 수행하지만, 지금 관심 있는 부분은 TCP 서버를 여는 흐름입니다. 이 관점에서만 보면 역할은 다음처럼 단순하게 정리할 수 있습니다.
- TCP 연결 핸들러를 생성합니다.
mysql.NewListener로 TCP 서버를 생성합니다.- goroutine으로
tcpListener.Accept를 호출합니다.
서버가 실제로 어떤 방식으로 생성되는지 확인하려면 mysql.NewListener 메서드를 보면 됩니다. 이 함수 내부를 보면 TCP 서버를 어떻게 여는지 바로 확인할 수 있습니다.
// https://github.com/vitessio/vitess/blob/94fdc736eae8928a8fdde44e9ec9c3bee1868d6f/go/mysql/server.go#L252
// NewListener creates a new Listener.
func NewListener(
protocol, address string,
authServer AuthServer,
handler Handler,
connReadTimeout time.Duration,
connWriteTimeout time.Duration,
proxyProtocol bool,
connBufferPooling bool,
keepAlivePeriod time.Duration,
flushDelay time.Duration,
) (*Listener, error) {
// ✅ net.Listen을 이용하여 golang 기본 net 패키지로 TCP 서버를 호스팅
listener, err := net.Listen(protocol, address)
if err != nil {
return nil, err
}
// ...
return NewFromListener(listener, authServer, handler, connReadTimeout, connWriteTimeout, connBufferPooling, keepAlivePeriod, flushDelay)
}
여기서 net.Listen을 호출해 Go 표준 라이브러리의 net 패키지로 직접 TCP 리스너를 생성한다는 점을 확인할 수 있습니다. 이후 이 리스너를 감싼 Listener를 반환하면서 인증 서버, 핸들러, 타임아웃 같은 설정을 함께 연결합니다. 다음으로 Listener.Accept를 보면, 들어온 TCP 연결을 받고 각 커넥션을 별도 goroutine으로 넘기는 흐름을 볼 수 있습니다.
// https://github.com/vitessio/vitess/blob/94fdc736eae8928a8fdde44e9ec9c3bee1868d6f/go/mysql/server.go#L328
// Accept runs an accept loop until the listener is closed.
func (l *Listener) Accept() {
ctx := context.Background()
for {
// ✅ 커넥션 연결 블로킹
conn, err := l.listener.Accept()
if err != nil { // ✅ 보통 Listener 종료 또는 Accept 실패 상황
// Close() was probably called.
connRefuse.Add(1)
return
}
acceptTime := time.Now()
connectionID := l.connectionID
l.connectionID++
// ✅ 커넥션 개수 기록
connCount.Add(1)
connAccept.Add(1)
go func() {
// ...
// ✅ 실제 로직 함수 handle 호출
l.handle(conn, connectionID, acceptTime)
}()
}
}
이 함수는 내부에서 net.Listener의 Accept를 호출해 블로킹 상태로 소켓 연결을 대기합니다. 연결이 수락되면 수락 시각과 커넥션 ID를 기록한 뒤, 해당 소켓을 처리하는 l.handle을 각 커넥션마다 별도 goroutine으로 실행합니다. 즉 클라이언트 연결이 N개라면, 이를 처리하는 goroutine도 기본적으로 N개가 생깁니다. 또한 MySQL 서버 쿼리 처리의 핵심 로직은 결국 l.handle 내부에 있다는 점도 여기서 확인할 수 있습니다.
다음 글에서는 l.handle 내부를 따라가며, 연결된 소켓이 실제로 어떻게 처리되는지 살펴보겠습니다.
참고자료
| [Vitess | VTGate](https://vitess.io/docs/archive/19.0/concepts/vtgate/) |
Leave a comment