MySQL Connector/Python'u Üretim İçin Yapılandırmak

Selamlar, bu yazımda MySQL Connector/Python'u local'de oyuncak gibi çalıştırmaktan çıkarıp, üretimde gece 3'te uyandırmayacak hale nasıl getiriyoruz, ona bakacağız. Konu kuru görünüyor ama açıkçası bir bağlantının nasıl açıldığı, nasıl kapandığı ve charset'in ne olduğu - işin %80'i burada bitiyor. Hadi başlayalım.

Connector/Python neden bu kadar konuşuluyor?

mysql-connector-python, Oracle'ın resmi sürücüsü. İki ana alternatifi var: C üzerine kurulu mysqlclient (en hızlısı) ve saf Python yazılmış PyMySQL (en taşınabilir olanı). Bence resmi sürücüyü tercih etmenin en büyük sebebi MySQL'in yeni özelliklerine ilk önce burada destek gelmesi. Performans canınızı sıkıyorsa mysqlclient'a geçersiniz, ama 'hangisi daha hızlı?' tartışmasına girmeden önce çoğu projede darboğazın sürücü olmadığını söyleyeyim.

Kurulum kısmı klasik:

# Resmi MySQL surucusu
pip install mysql-connector-python

# C uzantili, daha hizli alternatif
pip install mysqlclient

# Saf Python alternatif
pip install PyMySQL

Temel bağlantı: charset'i unutmayın

İlk kuralım: utf8 yazan her yere ikna olmadan bakın. MySQL'in efsane tuzaklarından biri, utf8 aliasının aslında 3-byte'lık eski bir kodlama olmasıdır. Emoji'ler, bazı CJK karakterler, hatta düz Türkçe metnin bazı kombinasyonları orada patlar. utf8mb4 istiyorsunuz, başka bir şey değil.

import mysql.connector

conn = mysql.connector.connect(
    host='db.example.com',
    port=3306,
    database='myapp',
    user='app_user',
    password='app_password',
    charset='utf8mb4',
    collation='utf8mb4_unicode_ci',
    use_unicode=True,
    time_zone='+00:00',
    connection_timeout=10,
    autocommit=False,
)

time_zone='+00:00' da önemli. Sunucunuz UTC olsun, uygulamada lokalize edin; aksi halde 'cron 03:00'da çalışıyor ama veritabanı 06:00 yazıyor' tarzı saatlik debug tuzaklarına düşersiniz.

Bağlantı havuzu - üretimde tek başına bağlantı açmayın

Her isteğin TCP el sıkışmasını, kimlik doğrulamasını ve charset müzakeresini baştan yapması üretimde kabul edilemez. Havuz şart:

import mysql.connector.pooling

pool = mysql.connector.pooling.MySQLConnectionPool(
    pool_name='myapp_pool',
    pool_size=20,
    pool_reset_session=True,
    host='db.example.com',
    port=3306,
    database='myapp',
    user='app_user',
    password='app_password',
    charset='utf8mb4',
    collation='utf8mb4_unicode_ci',
    use_unicode=True,
    time_zone='+00:00',
    connection_timeout=10,
    autocommit=False,
)

def get_orders(user_id: int):
    conn = pool.get_connection()
    try:
        cursor = conn.cursor(dictionary=True)
        cursor.execute(
            'SELECT id, total, status FROM orders WHERE user_id = %s',
            (user_id,),
        )
        return cursor.fetchall()
    finally:
        cursor.close()
        conn.close()  # Baglantiyi havuza geri verir

Buradaki tek kritik nokta: conn.close() aslında bağlantıyı kapatmıyor, havuza geri veriyor. try/finally bloğunu atlamayın; aksi halde havuzunuz birkaç dakika içinde tükenir ve uygulamanız kilitlenir.

SSL: opsiyonel değil

Veritabanınız aynı sunucuda olmadıkça SSL pazarlık konusu değil. Sertifikalarla:

conn = mysql.connector.connect(
    host='db.example.com',
    database='myapp',
    user='app_user',
    password='app_password',
    ssl_ca='/etc/ssl/mysql/ca.pem',
    ssl_cert='/etc/ssl/mysql/client-cert.pem',
    ssl_key='/etc/ssl/mysql/client-key.pem',
    ssl_verify_cert=True,
    ssl_verify_identity=True,
)

ssl_verify_identity=True'yu açmazsanız ortadaki adam saldırısına karşı kapı yarı açık kalır. Açın.

SQLAlchemy ile daha temiz bir yol

Birden fazla servis, birden fazla query, birden fazla migration - elle havuz yönetmek bir yerden sonra yorucu. SQLAlchemy'nin QueuePool'u tam olarak bu iş için var:

from sqlalchemy import create_engine, text
from sqlalchemy.pool import QueuePool
import os

engine = create_engine(
    f'mysql+mysqlconnector://{os.environ["DB_USER"]}:{os.environ["DB_PASSWORD"]}'
    f'@{os.environ["DB_HOST"]}:3306/{os.environ["DB_NAME"]}',
    poolclass=QueuePool,
    pool_size=20,
    max_overflow=10,
    pool_timeout=30,
    pool_recycle=1800,
    pool_pre_ping=True,
    connect_args={
        'charset': 'utf8mb4',
        'time_zone': '+00:00',
        'connection_timeout': 10,
    },
)

pool_pre_ping=True benim için pazarlıksız. MySQL sunucusu bağlantıyı wait_timeout sonrası sessizce düşürür; ping olmadan bunu ancak ilk sorgunuz Lost connection ile patladığında öğrenirsiniz. pool_recycle=1800 ise 30 dakikayı geçen bağlantıları proaktif olarak yeniler.

Sık karşılaşılan hatalar

  • utf8 yazıp bitti sanmak: utf8mb4 olmazsa emoji ve bazı diakritikler kaybolur. Hem bağlantıda hem tablo seviyesinde tutarlı olmalı.
  • Havuzdan aldığın bağlantıyı close etmemek: Havuz tükenir, PoolError: Failed getting connection görürsünüz. finally bloğu zorunlu.
  • autocommit=True ile transaction beklemek: Birden fazla INSERT'i atomik sanırsanız, biri patladığında geriye dönüş yok. Açıkça False yapın, commit/rollback'i siz yönetin.
  • Deadlock'u retry'lamamak: MySQL 1213 koduyla normal işleyişin parçası olarak deadlock döndürür. Yakalayın, rollback yapın, kısa bir bekleyişle tekrar deneyin.

Kapanış

Üretimdeki MySQL bağlantınız aslında üç şeyden ibaret: doğru charset, sağlıklı bir havuz ve şifreli bir kanal. Bence buradaki hiçbir ayarı 'sonra bakarım' diye ertelememek lazım, çünkü hepsi production'da bir kere ısırınca ders veren konular. Umarım faydalı olur, görüşmek üzere.