클라우드 클럽에서 Flnnel 오픈소스 분석 1주차로, 나는 backend 패키지 하위의 extension과 tencentvpc 코드 분석을 맡게 되었다.
해당 코드를 공부하는 것은.. 네트워크 관련 지식을 쌓는 것이 목적이다. 업무 자체가 한정된 시간내에 임팩트를 내야하는 것이 많다보니 low 레벨까지 내려가서 우직하게 공부하는 경험이 많이 없었다. (정상인가?) 여튼 스터디하며 기술적으로 성장하기를 바란다!!
extension 패키지의 목적은 사용자가 자신만의 네트워크 구성을 정의할 수 있게 해주는 커스텀 백엔드이다. 그러니까.. Flannel이 네트워크를 만들 때.. Route table, Subnet, priave IP, NIC 등등 을 구성할 때 커스텀한 설정을 넣을 수 있도록 하는 것이다.
등등 의 경우라고 한다... (feat. Claude)
extension 패키지에는 두 개의 파일이 있다. 먼저 extension_network.go
type network struct {
extIface *backend.ExternalInterface
lease *lease.Lease
sm subnet.Manager
preStartupCommand string
postStartupCommand string
subnetAddCommand string
subnetRemoveCommand string
}
파일을 열자마자 보이는것은 network 구조체이다. 여러 프로퍼티와 메소드를 가지고 있는데 눈에 띄는 것은 lease 라는 것이다.
Leases
Distributed systems often have a need for leases, which provide a mechanism to lock shared resources and coordinate activity between members of a set. In Kubernetes, the lease concept is represented by Lease objects in the coordination.k8s.io API Group, which are used for system-critical capabilities such as node heartbeats and component-level leader election.
공식문서의 설명에 따르면.. 공유자원을 lock하는 메커니즘이라고.. 한다. 생각해보면 여러 노드가 동시에 생성될 때 같은 서브넷을 할당받는 문제가 있을 수 있고, 이러면 파드간 통신에 장애가 된다.
그래서 lease 라는 구조체에는 프로퍼티로 subent이 들어가있다.
type Lease struct {
EnableIPv4 bool
EnableIPv6 bool
Subnet ip.IP4Net
IPv6Subnet ip.IP6Net
Attrs LeaseAttrs
Expiration time.Time
Asof int64 //Only used in etcd
}
CNI가 필요한 핵심적인 이유는 서로 다른 노드에 있는 파드를 찾아내고 통신할 수 있게 하기 위함이었다.
전통적인 TCP/IP 기반 통신 구조에서 두 개의 컴퓨터는 서로를 IP 기반으로 찾아내었다. 이러한 통신 구조를 호환하기위해 쿠버네티스에서는 가상이지만 파드마다 IP를 부여하게 된다. 이로 인해 애플리케이션들이 네트워크 설정을 특별히 신경 쓰지 않고도, 다른 Pod와 IP 기반의 표준 통신을 할 수 있게 된다!
패킷은 결국 파드가 스케줄링되어서 돌아가는 노드로 보내져야한다. 그래서 파드별로 IP가 부여되더라도, 그 파드가 어떤 노드에 있는지 알기 위해 서브넷으로 구분하여 노드를 관리한다.
서브넷은 하나의 IP 주소를 범위에 따라 나누어 여러 개의 작은 네트워크로 쪼갠 것이니, 파드 IP가 속한 서브넷 대역에 따라 어떤 노드에 있는지 구분하기 쉬워지고 IP 충돌 또한 방지할 수 있다.
그래서 리스는 노드가 사용할 서브넷 정보를 담고 있으며 해당 정보를 다른 노드들과 공유되어 서브넷 중복을 방지하는 역할을 한다.
Node A ETCD Node B
| | |
|---(Lease Request)----->| |
|<---(Subnet Lease)------| |
| |<---(Lease Request)-----|
| |---(Different Subnet)-->|
| | |
|---(Heartbeat/Renew)-->| |
코드를 좀 더 살펴보면 Run 이라는 메소드가 있다
func (n *network) Run(ctx context.Context) {
wg := sync.WaitGroup{}
log.Info("Watching for new subnet leases")
evts := make(chan []lease.Event)
wg.Add(1)
go func() {
subnet.WatchLeases(ctx, n.sm, n.lease, evts)
wg.Done()
}()
defer wg.Wait()
for {
evtBatch, ok := <-evts
if !ok {
log.Infof("evts chan closed")
return
}
n.handleSubnetEvents(evtBatch)
}
}
로그 자체에도 써져있듯이 ("Watching for new subnet leases)
이것은 노드의 네트워크 설정을 지속적으로 모니터링하고
다른 노드들의 서브넷 변경사항을 추적하는 역할을 한다. 고루틴을 사용하여 새로운 서브넷 리스를 비동기적으로 모니터링하는데, 이를 통해 노드간 서브넷 충돌을 방지할 수 있다.
서브넷관련 이벤트를 받아.. 서브넷이 추가, 제거되었을 때 처리하는 함수
주로 extension 타입의 서브넷인지 확인하고 환경변수 설정 (SUBNET, PUBLIC_IP) 한다.
func (n *network) handleSubnetEvents(batch []lease.Event) {
for _, evt := range batch {
switch evt.Type {
case lease.EventAdded:
log.Infof("Subnet added: %v via %v", evt.Lease.Subnet, evt.Lease.Attrs.PublicIP)
if evt.Lease.Attrs.BackendType != "extension" {
log.Warningf("Ignoring non-extension subnet: type=%v", evt.Lease.Attrs.BackendType)
continue
}
if len(n.subnetAddCommand) > 0 {
backendData := ""
if len(evt.Lease.Attrs.BackendData) > 0 {
if err := json.Unmarshal(evt.Lease.Attrs.BackendData, &backendData); err != nil {
log.Errorf("failed to unmarshal BackendData: %v", err)
continue
}
}
cmd_output, err := runCmd([]string{
fmt.Sprintf("SUBNET=%s", evt.Lease.Subnet),
fmt.Sprintf("PUBLIC_IP=%s", evt.Lease.Attrs.PublicIP)},
backendData,
"sh", "-c", n.subnetAddCommand)
if err != nil {
log.Errorf("failed to run command: %s Err: %v Output: %s", n.subnetAddCommand, err, cmd_output)
} else {
log.Infof("Ran command: %s\n Output: %s", n.subnetAddCommand, cmd_output)
}
}
extension_network가 서브넷 추가/제거 시 후속 작업.. 그리고 런타임에서의 네트워크 변경 처리를 했다면 여기서는 네트워크 초기 설정, 라우팅 테이블 구성, 인터페이스 설정, 서브넷 할당, 기본 네트워크 구성을 담당한다.
네트워크 구축시 커스텀한 옵션을 추가하기 때문에.. 기본 backend 타입이 아닌 ExtensionBackend 타입이 선언되어있다.
type ExtensionBackend struct {
sm subnet.Manager
extIface *backend.ExternalInterface
networks map[string]*network
}
이 객체를 만들게 되면 해당 객체는 서브넷 할당/회수, IP 주소 관리 ,네트워크 인터페이스 설정, 네트워크 상태 추적 등등의 역할을 하게 된다.
이 패키지에서 핵심이 되는 메소드로 말그대로 네트워크를 등록하는 함수이다.
func (be *ExtensionBackend) RegisterNetwork(ctx context.Context, wg *sync.WaitGroup, config *subnet.Config) (backend.Network, error) {
n := &network{
extIface: be.extIface,
sm: be.sm,
}
// Parse out configuration
if len(config.Backend) > 0 {
cfg := struct {
PreStartupCommand string
PostStartupCommand string
SubnetAddCommand string
SubnetRemoveCommand string
}{}
if err := json.Unmarshal(config.Backend, &cfg); err != nil {
return nil, fmt.Errorf("error decoding backend config: %v", err)
}
n.preStartupCommand = cfg.PreStartupCommand
n.postStartupCommand = cfg.PostStartupCommand
n.subnetAddCommand = cfg.SubnetAddCommand
n.subnetRemoveCommand = cfg.SubnetRemoveCommand
}
기본적으로 네트워크 설정에서 커스텀하고 싶은 명령어를 파싱해서 configuration에 등록해주는 역할을 한다.
이후 주목할점은 lease 처리 부분인데, 서브넷 매니저가 lease를 획득하는 로직이 있다. (아마 AcquireLease 함수 부분은 매우 복잡할거같다..)
이를 통해 네트워크는 자신만의 고유한 서브넷 범위를 가지고 다른 노드들과 서브넷 충돌 없이 운영할 수 있다 !!
// 1. 먼저 네트워크 객체 생성
n := &network{
extIface: be.extIface, // 외부 인터페이스 정보
sm: be.sm, // 서브넷 매니저
}
// 2. 리스 설정
attrs := lease.LeaseAttrs{
BackendType: "extension",
BackendData: data,
}
// 3. 리스 획득
lease, err := be.sm.AcquireLease(ctx, &attrs)
// 4. 획득한 리스를 네트워크 객체에 할당
switch err {
case nil:
n.lease = lease // 여기서 네트워크에 리스 정보 저장
자 이제 요약해보면..
[새로운 노드 생성]
↓
[네트워크 객체 생성]
↓
[서브넷 매니저에 리스 요청]
| → 1. BackendType: "extension" 지정
| → 2. PublicIP 설정
| → 3. 사용 가능한 서브넷 찾기
↓
[리스 획득]
| → 서브넷 매니저가 충돌 없는 서브넷 할당
↓
[네트워크 객체에 리스 저장]
↓
[네트워크 설정 실행]
| → 1. preStartupCommand 실행 (사용자 정의 초기 설정)
| → 2. postStartupCommand 실행 (서브넷/IP 기반 설정)
↓
[서브넷 이벤트 모니터링 시작]
| → 1. 새로운 서브넷 추가 감지 (EventAdded)
| → 2. 서브넷 제거 감지 (EventRemoved)
| → 3. subnetAddCommand/subnetRemoveCommand 실행
↓
[컨테이너 네트워크 준비 완료]
| → 1. 할당된 서브넷으로 컨테이너 IP 할당 가능
| → 2. 노드 간 통신 가능
이런 구조로 네트워크 설정이 될 것이다 !!