Eylül 2019'da bir GSM faturası neredeyse protokol mimarimizi benden önce tasarlamıştı. Saha ekibi 200 adet yeni nesil cihazı pilot filoya takmış, ay sonunda muhasebe M2M hattı başına veri tüketiminin eski cihazların üç katına çıktığını fark etmişti. Cihaz başına ayda 18 MB yerine 55 MB. Binlerce cihaza ölçeklendiğinde bu fark, yıllık ciddi bir kalem demekti. Yeni cihazların tek büyük farkı vardı: eski nesil bizim el yapımı ikili TCP protokolümüzle konuşuyordu, yenileri MQTT ile.
"MQTT verimli bir IoT protokolüdür" cümlesini her yerde okursunuz. Doğru da. Ama verimlilik, onu nasıl kullandığınıza acımasızca bağlı ve biz ilk denemede hemen her ayarı yanlış yapmıştık.
El Yapımı Protokolün Konforu ve Kamburu
Önce eski dünyayı anlatayım. İlk nesil cihazlarımız düz TCP soketi açıyor, kendi ikili çerçevemizle konuşuyordu: 2 bayt sihirli sayı, 2 bayt uzunluk, 1 bayt mesaj tipi, veri, 2 bayt CRC. Bir konum paketi topu topu 34 bayttı. Sunucu tarafında epoll tabanlı tek bir soket sunucusu bu bağlantıları tutuyordu. Bayt bayt kontrol bizdeydi; GPRS'in cimri dünyasında bu paha biçilmezdi.
Kamburu ise protokolün etrafına ördüğümüz her şeydi. Teslim garantisi mi? Kendi ACK ve tekrar gönderim mantığımızı yazdık, üç firmware sürümünde üç farklı hata çıkardı. Cihaza komut göndermek mi (röle kes, aralık değiştir)? Bağlantının o an ayakta olup olmadığını takip eden, değilse komutu kuyruklayan koca bir katman. Yeni mesaj tipi eklemek, cihaz ve sunucu ekiplerinin haftalarca senkron çalışması demekti. MQTT'nin bize sattığı şey bayt tasarrufu değil, bu katmanların hazır gelmesiydi: broker, topic'ler, QoS, retained mesajlar, Last Will ile kopuş tespiti.
Faturayı Şişiren Üç Zanlı
Peki 55 MB nereden geliyordu? Trafiği Wireshark'a döküp cihaz başına bayt muhasebesi çıkardık. Zanlı bir: keep-alive furyası. Entegratör firmanın örnek kodundan kalan ayarla cihazlar 30 saniyede bir PINGREQ atıyordu. PINGREQ+PINGRESP kendisi 4 bayt ama her paket TCP/IP başlıklarıyla birlikte teller üzerinde 80-90 bayta mal oluyor; günde 2.880 ping, tek başına ayda 7 MB'tan fazla. Zanlı iki: topic israfı. Her konum mesajı fleet/vehicles/{imei}/telemetry/position/v1 gibi 48 karakterlik bir topic'e basılıyordu ve MQTT PUBLISH paketinde topic her seferinde tam metin gider. Zanlı üç: veri JSON'du. Alan adlarıyla birlikte konum mesajı 176 bayt tutuyordu; eski ikili çerçevede 34 bayt olan şey.
Üçünü de tek tek düzelttik. Keep-alive'ı operatörün NAT zaman aşımına göre ayarladık — burada bir saha gerçeği var: Türkiye'deki GSM operatörlerinin CGNAT'ları boştaki bağlantıyı bizim ölçtüğümüz kadarıyla 5 ila 15 dakika arasında düşürüyor ve bu süre operatöre, hatta APN'e göre değişiyor. 240 saniyelik keep-alive üç operatörde de güvenli çıktı. Topic'leri fv/{kısa-id}/p formuna indirdik. JSON'u da attık; MQTT yükün içeriğine karışmaz, biz de payload olarak eski ikili çerçevemizin sadeleştirilmiş halini gömdük. En iyi iki dünya: MQTT'nin oturum yönetimi, bizim bayt cimriliğimiz.
QoS 1'in Küçük Yazılmış Satırları
Teslim garantisi için QoS 1 seçtik: broker PUBACK dönene kadar cihaz mesajı saklıyor ve tekrarlıyor. Kâğıt üstünde tertemiz. Sahada iki sürprizi oldu. Birincisi, QoS 1 "en az bir kez" demek — tünelden çıkan cihaz, ACK'i alamadığı 40 mesajı yeniden bastığında sunucu tarafında mükerrer kayıtlar oluştu. Konum verisinde mükerrer kayıt zararsız görünür ama kilometre hesabına çift girince değil. Payload'a cihaz tarafı sıra numarası ekleyip işleme katmanını idempotent yaptık; aynı (cihaz, sıra no) ikilisi ikinci kez gelirse sessizce düşüyor.
QoS 2'yi ("tam bir kez") hiç ciddi düşünmedik; dört yollu el sıkışması her mesajın bayt ve gecikme maliyetini ikiye katlıyor ve GPRS'te kopan bağlantılarda yarım kalan el sıkışmalar ayrı bir dert. Mükerrer kaydı sunucuda idempotency ile çözmek, teli üzerinde garanti satın almaktan her zaman daha ucuz. Bu arada Last Will de beklenmedik bir hediye oldu: her cihaz bağlanırken broker'a "ben düşersem şu topic'e offline yaz" vasiyetini bırakıyor. Cihazın çevrimiçi/çevrimdışı durumunu izleyen kod, bizim eski sistemde soket zaman aşımlarını kollayan 400 satırlık bir modüldü; MQTT'de bir retained mesaj ve bir vasiyetten ibaret.
İkinci sürpriz clean session bayrağıydı. İlk kurulumda true bırakmışız; cihaz her yeniden bağlandığında broker'daki oturumu sıfırlanıyor, kopukluk sırasında ona gönderilmiş komutlar buharlaşıyordu. Müşteri "röle kes komutu gitti mi gitmedi mi" diye sorduğunda "bağlantıya denk geldiyse gitti" cevabı verilemez. clean session'ı false yapıp komut topic'lerine QoS 1 abonelik verince broker kopukluk boyunca komutu tuttu, cihaz dönünce teslim etti. Bu tek bayrak, bizim eski sistemde üç ayda yazdığımız komut kuyruğu katmanının tamamını emekliye ayırdı.
Karar: İki Protokol, Tek İlke
Broker olarak Mosquitto ile başladık; 3.000 cihazda tek makinede kılını kıpırdatmadı (CPU yüzde 6, RAM 400 MB civarı). Bizi asıl uğraştıran broker değil, broker'ın arkası oldu: mesajları MQTT'den alıp işleme hattına güvenilir biçimde aktaran köprü süreci. İlk sürümü mesajı alıp senkron olarak veritabanına yazıyordu; veritabanı yavaşlayınca köprü yavaşlıyor, broker'da kuyruk şişiyordu. Köprüyü araya tampon kuyruk koyarak ayırdık — bu da başka bir yazının konusu.
Peki eski cihazlar? 9.000 küsur sahada çalışan cihazı MQTT'ye taşımak firmware güncellemesi demekti ve saha firmware güncellemesi, tecrübeyle sabit, her 100 cihazdan 2-3'ünü tuğlaya çevirme riski taşır. Taşımadık. Eski nesil ham TCP sunucusuyla yaşamaya devam etti, tüm yeni cihazlar MQTT'yle geldi. İki yıl boyunca iki protokolü yan yana işlettik ve bu "temiz olmayan" karardan hiç pişman olmadık. Mimari saflık, çalışan 9.000 cihazı riske atmaya değmez.
Bir itiraf: pilot dönemde TLS kullanmadık, kapalı APN'in sağladığı izolasyona yaslandık. 2019'da GPRS modemlerde TLS el sıkışması hem 4-6 KB ek trafik hem de zayıf çekim bölgelerinde 10 saniyeye varan bağlantı süresi demekti; kopup yeniden bağlanan bir cihaz için bu maliyet her seferinde tekrar ödenir. Kapalı APN + kullanıcı adı/parola + broker tarafında IMEI beyaz listesi, o günün şartlarında bilinçli bir dengeydi. Bugün aynı kararı verir miydim ayrı tartışma; ama "TLS yoksa proje çöp" diyen tribün diline de mesafeliyim. Tehdit modeli yazılır, maliyet ölçülür, karar verilir.
Ay sonu faturasına dönersek: düzeltmeler sonrası MQTT cihazlarının tüketimi 55 MB'tan 11 MB'a indi — evet, eski ham TCP'nin de altına. Çünkü eski protokolde bizim ACK/tekrar katmanımız kopuk bağlantılarda körlemesine tekrar yapıyordu; MQTT'nin oturum bilinci bu israfı da yok etti. Ders: protokol seçimi bir kimlik meselesi değil, ölçüm meselesi. Kendi cihaz filonuz için bu hesabı yapmak, keep-alive ile NAT arasındaki dengeyi kurmak isterseniz yazın, konuşalım; Wireshark dökümü okumak hâlâ en sevdiğim mesai dışı aktivite.