"PostgreSQL harika bir veritabanı... ama kesinti yaşandığında? Patroni burada devreye giriyor."
Giriş: Dağıtık Sistemlerde PostgreSQL'in Derdi
PostgreSQL, tek başına son derece güvenilir ve performanslı bir veritabanıdır. Ancak işler büyüdüğünde, yani veritabanını dağıtık bir sistem içinde birden fazla sunucuda yüksek erişilebilir şekilde kullanmak istediğinizde, birtakım zorluklar ortaya çıkar:
- Lider (primary) sunucu arızalanırsa ne olacak?
- Yedek sunucular (replica) ne zaman devreye girmeli?
- Manuel müdahale mi gerekiyor?
- Veri tutarlılığı ve erişilebilirlik arasında denge nasıl sağlanacak?
İşte bu gibi durumlarda Patroni hayat kurtarıyor.
Patroni Nedir, Ne Yapar?
Patroni, PostgreSQL için otomatik failover, leader election (lider seçimi), health check ve cluster yönetimi sağlayan bir HA (High Availability) yöneticisidir. Aslında Patroni, PostgreSQL'e HA kazandıran akıllı bir düzenleyici (orchestrator)'dir. Basit bir anlatımla Patroni:
- Etcd veya Consul gibi bir dağıtık KV store ile küme durumunu tutar,
- Sunucuların sağlığını izler,
- Lider düştüğünde otomatik olarak yeni bir lider seçer ve şekli değiştirir,
- PostgreSQL'in replikasyon ayarlarını dinamik olarak düzenler.
Bu yazıda HAProxy veya PgBouncer gibi ara katmanlardan bahsetmeyeceğiz. Onlara özel yazılar yakında yayınlanacak, takipte kalın!
CAP Teoremi ve Patroni
CAP teoremi der ki: Üç şeyden sadece ikisini seçebilirsin:
1. Consistency (Tutarlılık)
2. Availability (Erişilebilirlik)
3. Partition Tolerance (Bölünebilirlik toleransı)
Dağıtık sistemlerde ağ bölünmeleri (partition) kaçınılmaz olduğuna göre, geriye tutarlılık (Consistency) ile erişilebilirlik (Availability) arasında bir seçim yapmak kalıyor. Patroni, "Consistency + Partition Tolerance" tarafını seçer. Neden?
- Birden fazla primary (yazılabilir) sunucuya izin vermez.
- Yazılacak verilerin tutarlığını korumaya öncelik verir.
- Ancak bu durum, bazen sistemi yazma işlemleri için geçici olarak erişilemez hale getirebilir.
Etcd ve Consul: Patroni'nin Dayandığı Taşlar
Patroni, bir liderin kim olduğunu ve cluster'ın genel sağlık durumunu merkezi olarak tutmak için bir KV Store'a ihtiyaç duyar. Bu genellikle:
Bu sistemler:
- RAFT protokolü ile dağıtık bir consensus sağlar,
- Yüksek erişilebilirlik sağlamak için en az 3 node ile çalışırlar,
- Patroni her sunucuda bu KV store'a yazma ve okuma işlemleri yapar.
Lider Seçimi Nasıl Gerçekleşirir?
Patroni, lider seçimini Etcd üzerinden gerçekleştirir. Süreç şuna benzer:
1. Her PostgreSQL örneği, bağlı olduğu Patroni botu aracılığıyla Etcd'de belirli bir anahtarın süresi dolmuş olup olmadığını kontrol eder.
2. Eğer liderlik anahtarı boştaysa, bot bu anahtarı belirli bir süreliğine almak için Etcd'ye başvurur.
3. Etcd, arka planda RAFT protokolü kullanarak yalnızca bir örneğin bu anahtarı almasına izin verir. Böylece yarış durumları engellenmiş olur.
4. Anahtarı ilk alan örnek, sistemin lideri olarak kabul edilir ve Patroni botu bu PostgreSQL örneğini primary (lider) olacak şekilde yapılandırır.
5. Diğer örnekler, bu liderliği fark eder ve kendilerini otomatik olarak replica (yedek) moduna alır.
Bu sayede lider, merkezi bir kontrol noktası olmadan dağıtık biçimde ve tutarlı şekilde seçilir.
Kısıtlamalar ve Sınırlar
- Single Primary mimarisi: Aynı anda yalnızca bir sunucu yazılabilir durumda olur.
- Etcd/Consul'a bağımlılık: Bu sistemler de HA olmalı. Aksi halde Patroni de çalışamaz.
- Split-brain önleme: Yanlış lider seçimi olmaması için karmaşık kontrol mekanizmaları gerekir.
Docker ile Patroni Kurulumu (Minimal Örnek)
Basit bir örnek için 3 PostgreSQL + Patroni konteyneri ve 3 etcd konteyneri kullanabiliriz:
Öncelikle geliştirici github reposundan proje dosyasını temin ediniz. Proje dosyasındaki Dockerfile'ı build ederek örnekte kullanacağımız patroni docker image'ını oluşturunuz.
# docker-compose.yml version: "2" networks: demo: external: true services: etcd1: &etcd image: patroni networks: demo: ipv4_address: 10.0.0.11 environment: ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380 ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379 ETCD_INITIAL_CLUSTER: etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380 ETCD_INITIAL_CLUSTER_STATE: new ETCD_INITIAL_CLUSTER_TOKEN: tutorial ETCD_UNSUPPORTED_ARCH: arm64 container_name: demo-etcd1 hostname: etcd1 ports: - 1379:2379 command: etcd -name etcd1 -initial-advertise-peer-urls http://etcd1:2380 etcd2: <<: *etcd container_name: demo-etcd2 hostname: etcd2 networks: demo: ipv4_address: 10.0.0.12 ports: - 2379:2379 command: etcd -name etcd2 -initial-advertise-peer-urls http://etcd2:2380 etcd3: <<: *etcd container_name: demo-etcd3 hostname: etcd3 networks: demo: ipv4_address: 10.0.0.13 ports: - 3379:2379 command: etcd -name etcd3 -initial-advertise-peer-urls http://etcd3:2380 patroni1: image: patroni networks: demo: ipv4_address: 10.0.0.21 env_file: docker/patroni.env hostname: patroni1 container_name: demo-patroni1 ports: - 1008:8008 - 1432:5432 volumes: - ./patroni1_istanbul.yml:/home/postgres/postgres0.yml environment: &patroni_env ETCDCTL_ENDPOINTS: http://etcd1:2379,http://etcd2:2379,http://etcd3:2379 PATRONI_ETCD3_HOSTS: "'etcd1:2379','etcd2:2379','etcd3:2379'" PATRONI_SCOPE: demo PATRONI_NAME: patroni1 patroni2: image: patroni networks: demo: ipv4_address: 10.0.0.22 env_file: docker/patroni.env hostname: patroni2 container_name: demo-patroni2 volumes: - ./patroni2_ankara.yml:/home/postgres/postgres0.yml ports: - 2008:8008 - 2432:5432 environment: <<: *patroni_env PATRONI_NAME: patroni2 patroni3: image: patroni networks: demo: ipv4_address: 10.0.0.23 env_file: docker/patroni.env hostname: patroni3 container_name: demo-patroni3 volumes: - ./patroni3_izmir.yml:/home/postgres/postgres0.yml ports: - 3008:8008 - 3432:5432 environment: <<: *patroni_env PATRONI_NAME: patroni3
# ---------- patroni_istanbul.yml ---------- restapi: listen: 0.0.0.0:8008 connect_address: patroni1:8008 authentication: username: admin password: admin # etcd: # hosts: # - etcd1:2379 # - etcd2:2379 # - etvd3:2379 bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 primary_start_timeout: 300 synchronous_mode: true synchronous_mode_strict: false synchronous_standby_names: "patroni2,patroni3" postgresql: use_pg_rewind: true pg_hba: - local all all trust - host replication replicator 0.0.0.0/0 md5 - host all all all md5 parameters: max_connections: 100 initdb: - locale: en_US.UTF-8 - encoding: UTF8 - data-checksums postgresql: listen: 0.0.0.0:5432 connect_address: patroni1:5432 data_dir: /home/postgres/data authentication: replication: username: replicator password: rep-pass superuser: username: postgres password: patroni rewind: username: rewind_user password: rewind_password parameters: unix_socket_directories: '..' tags: location: istanbul noloadbalance: false clonefrom: false nostream: false
# ---------- patroni_ankara.yml ---------- restapi: listen: 0.0.0.0:8008 connect_address: patroni2:8008 authentication: username: admin password: admin # etcd: # hosts: # - etcd1:2379 # - etcd2:2379 # - etcd3:2379 bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 primary_start_timeout: 300 synchronous_mode: true synchronous_mode_strict: false synchronous_standby_names: "patroni1,patroni3" postgresql: use_pg_rewind: true pg_hba: - local all all trust - host replication replicator 0.0.0.0/0 md5 - host all all all md5 parameters: max_connections: 100 initdb: - locale: en_US.UTF-8 - encoding: UTF8 - data-checksums postgresql: listen: 0.0.0.0:5432 connect_address: patroni2:5432 data_dir: /home/postgres/data authentication: replication: username: replicator password: rep-pass superuser: username: postgres password: patroni rewind: username: rewind_user password: rewind_password parameters: unix_socket_directories: '..' tags: location: ankara noloadbalance: false clonefrom: false nostream: false
# ---------- patroni_izmir.yml ---------- restapi: listen: 0.0.0.0:8008 connect_address: patroni3:8008 authentication: username: admin password: admin # etcd: # hosts: # - etcd1:2379 # - etcd2:2379 # - etcd3:2379 bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 primary_start_timeout: 300 synchronous_mode: true synchronous_mode_strict: false synchronous_standby_names: "patroni1,patroni2" postgresql: use_pg_rewind: true pg_hba: - local all all trust - host replication replicator 0.0.0.0/0 md5 - host all all all md5 parameters: max_connections: 100 initdb: - locale: en_US.UTF-8 - encoding: UTF8 - data-checksums postgresql: listen: 0.0.0.0:5432 connect_address: patroni3:5432 data_dir: /home/postgres/data authentication: replication: username: replicator password: rep-pass superuser: username: postgres password: patroni rewind: username: rewind_user password: rewind_password parameters: unix_socket_directories: '..' tags: location: izmir2 nofailover: false noloadbalance: false clonefrom: false nostream: false
Konuya dair örnek (Yazar tarafından oluşturulmuştur)
Kapanış ve Son Sözler
Patroni, PostgreSQL'i HA yapıya taşımak için oldukça etkili ve esnek bir çözümdür.
Bir sonraki yazımızda, Patroni cluster'ına HAProxy ile yönlendirme yapmayı ve PgBouncer ile bağlantı havuzu oluşturmayı anlatacağız. Kaçırmayın!
Takipte Kalın, Verileriniz Ayakta Kalsın!