paint-brush
TDEngine ve GraphQL ile Zaman Serisi Veritabanı Oluşturmaile@patrickheneise
641 okumalar
641 okumalar

TDEngine ve GraphQL ile Zaman Serisi Veritabanı Oluşturma

ile Patrick Heneise13m2023/10/22
Read on Terminal Reader

Çok uzun; Okumak

Bu makalede, bir TDEngine veritabanı ve tablolarının kurulumunu ve çeşitli istemcilerden ve uygulamalardan gelen verileri sorgulamamıza olanak tanıyan bir GraphQL şemasının nasıl oluşturulacağını ele alacağız.
featured image - TDEngine ve GraphQL ile Zaman Serisi Veritabanı Oluşturma
Patrick Heneise HackerNoon profile picture
0-item
1-item

Motivasyon ve Giriş

Nevados'taki yazılım ekibinin bir parçası olarak Nevados All Terrain Tracker® için bir operasyon ve izleme platformu inşa ediyoruz. Güneş takip cihazı, güneş panelini güneşe doğru yönlendiren bir cihazdır. Her güneş takip cihazı sürekli olarak mevcut açı, sıcaklık, voltajlar vb. gibi durum bilgilerini ve okumaları platformumuza gönderir ve bu bilgileri analiz ve görselleştirme için saklamamız gerekir. İzleyici her 5 saniyede bir veri gönderecek şekilde yapılandırılmışsa, izleyici başına günde 17.280 veri noktamız, izleyici başına ayda 518.400 veri noktamız olur. Bu pek çok bilgiyi özetliyor. Bu tür verilere "zaman serisi verileri" denir ve yazılımdaki tüm karmaşık problemler için olduğu gibi bunun da çeşitli çözümleri (Zaman Serisi Veritabanları) vardır. En ünlüleri InfluxDB ve TimescaleDB'dir. Platformumuz için IoT uygulamaları için optimize edilmiş ve SQL sorgu dili ile çalışan nispeten yeni bir ürün olan TDEngine ile çalışmaya karar verdik.


Bu karar için çeşitli argümanlar vardı: TDEngine

  • açık kaynaktır
  • IoT uygulamaları için optimize edilmiştir
  • aşina olduğumuz bir dil olan SQL'i kullanıyor
  • yönetilen bir hizmet olarak mevcuttur ve uygulamamızı oluşturmaya odaklanabiliriz
  • Docker aracılığıyla yerel olarak çalıştırmak kolaydır


Bu makalede, bir TDEngine veritabanı ve tablolarının kurulumunu ve çeşitli istemcilerden ve uygulamalardan gelen verileri sorgulamamıza olanak tanıyan bir GraphQL şemasının nasıl oluşturulacağını ele alacağız.

TDEngine'i kullanmaya başlama

TDEngine'i kullanmaya başlamanın en kolay yolu bulut hizmetlerini kullanmaktır. TDEngine'e gidin ve bir hesap oluşturun. Kullanabileceğimiz birkaç halka açık veritabanları var; bu da bir demo oluşturmak veya sorgularla denemeler yapmak için harika.


TDEngine'i yerel olarak çalıştırmak istiyorsanız, çeşitli kaynaklardan veri almak ve bunları sistem bilgileri, ping istatistikleri vb. gibi veritabanına göndermek için Docker imajını ve Telegraf'ı kullanabilirsiniz.

 version: '3.9' services: tdengine: restart: always image: tdengine/tdengine:latest hostname: tdengine container_name: tdengine ports: - 6030:6030 - 6041:6041 - 6043-6049:6043-6049 - 6043-6049:6043-6049/udp volumes: - data:/var/lib/taos telegraf: image: telegraf:latest links: - tdengine env_file: .env volumes: - ./telegraf.conf:/etc/telegraf/telegraf.conf


Telegraf yapılandırması için resmi belgelere ve Telegraf'taki TDEngine belgelerine göz atın. Kısacası, bir MQTT konusuna bağlanmak için bu şuna benzer:

 [agent] interval = "5s" round_interval = true omit_hostname = true [[processors.printer]] [[outputs.http]] url = "http://127.0.0.1:6041/influxdb/v1/write?db=telegraf" method = "POST" timeout = "5s" username = "root" password = "taosdata" data_format = "influx" [[inputs.mqtt_consumer]] topics = [ "devices/+/trackers", ]

Her şeyi yerel olarak ayarlamak ve veritabanının bilgilerle dolmasını beklemek yerine, bu makale için ABD'nin 5 büyük limanından gelen gemi hareketlerini içeren genel veritabanını kullanacağız.

TDEngine'i genel gemi hareketi verileriyle kullanma

Varsayılan olarak TDEngine'deki tablolar örtülü bir şemaya sahiptir; bu, şemanın veritabanına yazılan verilere uyum sağladığı anlamına gelir. Bu, önyükleme için harikadır, ancak sonunda gelen verilerle ilgili sorunlardan kaçınmak için açık bir şemaya geçmek istiyoruz. Alışmak biraz zaman alan şeylerden biri de Süper Masalar (kısaca "STable") konseptidir. TDEngine'de etiketler (anahtarlar) ve sütunlar (veriler) bulunur. Her tuş kombinasyonu için bir "tablo" oluşturulur. Tüm tablolar STable'da gruplandırılmıştır.

tdengine bulut tablolarını gösteren ekran görüntüsü


vessel veritabanına bakıldığında, ais_data adında birçok tablo içeren bir STable'ı var. Genellikle tablo bazında sorgulama yapmak istemiyoruz, ancak tüm tablolardan birikmiş verileri almak için her zaman STable'ı kullanıyoruz.


TDEngine, bir tablonun veya STable'ın şemasını incelememize olanak tanıyan DESCRIBE işlevine sahiptir. ais_data aşağıdaki şemaya sahiptir:

TDEngine tablo şemasını gösteren ekran görüntüsü


STable'ın iki anahtarı ve altı veri sütunu vardır. Anahtarlar mmsi ve name . Verileri sorgulamak için normal SQL ifadelerini kullanabiliriz:

 SELECT ts, name, latitude, longitude FROM vessel.ais_data LIMIT 100; ts name latitude longitude 2023-08-11T22:07:02.419Z GERONIMO 37.921673 -122.40928 2023-08-11T22:21:48.985Z GERONIMO 37.921688 -122.40926 2023-08-11T22:25:08.784Z GERONIMO 37.92169 -122.40926 ...


Zaman serisi verilerinin genellikle çok büyük olduğunu ve bu nedenle sonuç kümesini her zaman sınırlamamız gerektiğini unutmayın. Sonuçları anahtara göre gruplayan ve en son güncelleme anahtarlarını almak için yararlı olan PARTITION BY gibi kullanabileceğimiz birkaç zaman serisine özgü işlev vardır. Örneğin:

 SELECT last_row(ts, name, latitude, longitude) FROM vessel.ais_data PARTITION BY name; ts name latitude longitude 2023-09-08T13:09:34.951Z SAN SABA 29.375961 -94.86894 2023-09-07T18:05:01.230Z SELENA 33.678585 -118.1954 2023-09-01T17:23:24.145Z SOME TUESDAY 33.676563 -118.230606 ... 


tdengine çıktısını gösteren ekran görüntüsü


Daha fazla örnek için SQL Belgelerini okumanızı öneririm. Devam etmeden önce "Programlama", "Node.js"ye gidin ve TDENGINE_CLOUD_URL ve TDENGINE_CLOUD_TOKEN değişkenlerinizi alın.

Nexus.js, Fastify ve Mercurius ile GraphQL

GraphQL bugünlerde oldukça iyi biliniyor ve hakkında pek çok güzel makale var. Farklı kaynaklardan bilgi toplayıp işlerken teknolojiyi seçtik ve GraphQL bunları şeffaf bir şekilde tek bir API'de birleştirmemize olanak tanıyor.


Harika Fastify çerçevesini (şimdiye kadar Node.js uygulamaları için varsayılan seçimdir) ve Mercurius adaptörünü kullanacağız. Mercurius ve Fastify ekipleri kusursuz bir deneyim için birlikte çalıştı ve performansa odaklanan mükemmel bir GraphQL API seçimi. GraphQL Nexus, şema ve çözümleyicileri oluşturmaya/oluşturmaya yönelik bir araçtır, dolayısıyla her şeyi elle yazmamız gerekmez.


Yapılması gereken bir miktar kurulum kodu vb. var, bunları burada atlayacağım. Tam bir örneği GitHub'da bulabilirsiniz - tdengine-graphql-example .


Bu makalede oldukça spesifik olan iki konuyu detaylandırmak istiyorum:

  1. TDEngine Sorgu kitaplığı
  2. Nexus ile GraphQL şeması

TDEngine Sorgu kitaplığı

TDEngine, veritabanını sorgulamamızı sağlayan bir Node.js kütüphanesine sahiptir. Bu, sorguları bağlamayı ve göndermeyi kolaylaştırır, ne yazık ki yanıtlarla çalışmak biraz zordur. Bu yüzden küçük bir sarmalayıcı yazdık:


 'use strict' import tdengine from '@tdengine/rest' import { tdEngineToken, tdEngineUrl } from '../config.js' import parseFields from 'graphql-parse-fields' const { options: tdOptions, connect: tdConnect } = tdengine tdOptions.query = { token: tdEngineToken } tdOptions.url = tdEngineUrl export default function TdEngine(log) { this.log = log const conn = tdConnect(tdOptions) this.cursor = conn.cursor() } TdEngine.prototype.fetchData = async function fetchData(sql) { this.log.debug('fetchData()') this.log.debug(sql) const result = await this.cursor.query(sql) const data = result.getData() const errorCode = result.getErrCode() const columns = result.getMeta() if (errorCode !== 0) { this.log.error(`fetchData() error: ${result.getErrStr()}`) throw new Error(result.getErrStr()) } return data.map((r) => { const res = {} r.forEach((c, idx) => { const columnName = columns[idx].columnName .replace(/`/g, '') .replace('last_row(', '') .replace(')', '') if (c !== null) { res[columnName] = c } }) return res }) }


Bu, GraphQL bağlamına aktarılabilecek bir TDEngine nesnesi döndürür. Öncelikle bir SQL sorgusunu iletebileceğimiz ve sonuçları bir nesne dizisi olarak geri alabileceğimiz fetchData işlevini kullanacağız. TDEngine meta verileri (sütunlar), hataları ve verileri ayrı ayrı döndürür. Sütunları normal bir nesne listesine eşlemek için meta verileri kullanacağız. Buradaki özel bir durum last_row işlevidir. Sütunlar last_row(ts) , last_row(name) vb. olarak döndürülür ve niteliğin GraphQL şemasına 1:1 eşlenmesi için last_row kısmını kaldırmak istiyoruz. Bu, columnName.replace bölümünde yapılır.

GraphQL Şeması

Ne yazık ki TDEngine için Postgraphile gibi bir şema oluşturucu yok ve saf bir GraphQL şeması yazıp sürdürmek istemiyoruz, bu yüzden bize bu konuda yardımcı olması için Nexus.js'yi kullanacağız. İki temel türle başlayacağız: VesselMovement ve Timestamp (skaler bir türdür). Timestamp ve TDDate tarihi zaman damgası veya tarih dizesi olarak görüntülemek için iki farklı türdür. Bu, hangi formatın kullanılacağına karar verebileceği için istemci uygulaması için (ve geliştirme sırasında) faydalıdır. asNexusMethod türü VesselMovement şemasında bir fonksiyon olarak kullanmamıza olanak tanır. Orijinal ts zaman damgası değerini kullanmak için TDDate tam burada tür tanımında çözebiliriz.


 import { scalarType, objectType } from 'nexus' export const Timestamp = scalarType({ name: 'Timestamp', asNexusMethod: 'ts', description: 'TDEngine Timestamp', serialize(value) { return new Date(value).getTime() } }) export const TDDate = scalarType({ name: 'TDDate', asNexusMethod: 'tdDate', description: 'TDEngine Timestamp as Date', serialize(value) { return new Date(value).toJSON() } }) export const VesselMovement = objectType({ name: 'VesselMovement', definition(t) { t.ts('ts') t.tdDate('date', { resolve: (root) => root.ts }) t.string('mmsi') t.string('name') t.float('latitude') t.float('longitude') t.float('speed') t.float('heading') t.int('nav_status') } })


Zaman serisi türleri için, arayüzdeki ilişkisel ve zaman serisi türlerinin net bir şekilde ayrılması için Movement veya Series son ekini kullanırız.


Artık Sorguyu tanımlayabiliriz. TDEngine'den en son hareketleri almak için basit bir sorguyla başlayacağız:

 import { objectType } from 'nexus' export const GenericQueries = objectType({ name: 'Query', definition(t) { t.list.field('latestMovements', { type: 'VesselMovement', resolve: async (root, args, { tdEngine }, info) => { const fields = filterFields(info) return tdEngine.fetchData( `select last_row(${fields}) from vessel.ais_data partition by mmsi;` ) } }) } }) 


sorgunun graphiql çıktısını gösteren ekran görüntüsü


GraphiQL, API'yi test etmek ve şemayı keşfetmek için harika bir araçtır; bunu Mercurius'ta graphiql.enabled = true ileterek etkinleştirebilirsiniz. Sorgu ile mmsi göre gruplandırılmış gemilerin son hareketlerini görebiliriz. Yine de biraz daha ileri gidelim. GraphQL'in en büyük avantajlarından biri istemciye veya uygulamaya şeffaf bir katman olmasıdır. Birden fazla kaynaktan veri alıp bunları aynı şemada birleştirebiliriz.


Maalesef kapsamlı gemi bilgileri içeren kolay/ücretsiz bir API bulamadım. Sinay var, ancak Vessel yanıtlarında yalnızca mmsi ve imo name veriyorlar (ki bu zaten TDEngine'de var). Örnek olması açısından veritabanımızda name bulunmadığını ve Sinay'dan almamız gerektiğini varsayalım. imo ile bir geminin CO2 emisyonlarını da sorgulayabiliriz veya başka bir API, bir görüntüyü, bayrağı veya diğer bilgileri almak için kullanılabilir; bunların tümü Vessel tipinde birleştirilebilir.


 export const Vessel = objectType({ name: 'Vessel', definition(t) { t.string('mmsi') t.string('name') t.nullable.string('imo') t.list.field('movements', { type: 'VesselMovement' }) } })


Burada görebileceğiniz gibi TDEngine'den gelen zaman serisi verileriyle alan movements bir listesini dahil edebiliriz. Gemi bilgilerini almak için başka bir sorgu ekleyeceğiz ve çözümleyici, TDEngine ve Sinay'dan gelen verileri birleştirmemize olanak tanıyor:


 t.field('vessel', { type: 'Vessel', args: { mmsi: 'String' }, resolve: async (root, args, { tdEngine }, info) => { const waiting = [ getVesselInformation(args.mmsi), tdEngine.fetchData( `select * from vessel.ais_data where mmsi = '${args.mmsi}' order by ts desc limit 10;` ) ] const results = await Promise.all(waiting) return { ...results[0][0], movements: results[1] } } }) 


sorgunun graphiql çıktısını gösteren ekran görüntüsü

🎉 ve burada, talep ettiğimiz belirli bir gemi için TDEngine'den satırları döndüren, çalışan bir GraphQL API'miz var. getVesselInformation() Sinay'dan veri almak için kullanılan basit bir sarmalayıcıdır. TDEngine sonuçlarını movements özelliğine ekleyeceğiz ve GraphQL gerisini halledecek ve her şeyi şemaya eşleyecektir.

Not: SQL Enjeksiyonu

Herhangi bir SQL veritabanında olduğu gibi kullanıcı girişi konusunda dikkatli olmamız gerekir. Yukarıdaki örnekte doğrudan mmsi girişini kullanıyoruz, bu da bu sorguyu SQL enjeksiyonlarına karşı savunmasız hale getiriyor. Örnek olması açısından bunu şimdilik göz ardı edeceğiz, ancak "gerçek dünya" uygulamalarında kullanıcı girişini her zaman sterilize etmeliyiz. Dizeleri temizlemek için etrafta birkaç küçük kütüphane vardır; çoğu durumda yalnızca GraphQL'in bizim için kontrol ettiği sayılara (sayfalandırma, limit vb.) ve numaralandırmalara (sıralama düzeni) güveniriz.


Bunu belirttiği için Dmitry Zaets'e teşekkürler!

Optimizasyonlar

Bu yazının kapsamını aşan birkaç şey var ama kısaca bahsetmek istiyorum:

Nexus.js'nin manevi halefi olarak Pothos

Projeye başladığımızda GraphQL şemamızı oluşturmak için Nexus.js en iyi seçimdi. Her ne kadar kararlı ve bir bakıma özellikleri tamamlanmış olsa da, bakım ve güncellemelerden yoksundur. Biraz daha modern ve aktif olarak bakımı yapılan, Pothos adında eklenti tabanlı bir GraphQL şema oluşturucusu var. Yeni bir projeye başlıyorsanız muhtemelen Nexus.js yerine Pothos'u kullanmanızı öneririm.


Bunu belirttiği için Mo Sattler'a teşekkürler!

Saha Çözümleyicileri

Yukarıdaki Vessel çözümleyicide görebileceğiniz gibi, her iki veri kaynağı da anında getirilip işleniyor. Bu, sorgu yalnızca name içinse yanıt için movements yine de getireceğimiz anlamına gelir. Sorgu yalnızca movements içinse, yine de adı Sinay'dan alıyoruz ve potansiyel olarak talebin bedelini ödüyoruz.


Bu bir GraphQL anti-modelidir ve yalnızca istenen verileri getirmek için alan bilgilerini kullanarak performansı artırabiliriz. Çözümleyicilerin dördüncü argümanı alan bilgisidir, ancak bunlarla çalışmak oldukça zordur. Bunun yerine, talep edilen alanların basit bir nesnesini almak ve çözümleyici mantığını ayarlamak için graphql-parse-fields kullanabiliriz.

SQL Sorgu Optimizasyonları

Örnek sorgularımızda, ihtiyaç duyulmasa bile veritabanındaki tüm sütunları getirmek için select * komutunu kullanıyoruz. Bu açıkça oldukça kötü ve sql sorgularını optimize etmek için aynı alan ayrıştırıcısını kullanabiliriz:


 export function filterFields(info, context) { const invalidFields = ['__typename', 'date'] const parsedFields = parseFields(info) const fields = context ? parsedFields[context] : parsedFields const filteredFields = Object.keys(fields).filter( (f) => !invalidFields.includes(f) ) return filteredFields.join(',') }


Bu işlev, GraphQL bilgisindeki alanların virgülle ayrılmış bir listesini döndürür.

 const fields = filterFields(info) return tdEngine.fetchData( `select last_row(${fields}) from vessel.ais_data partition by mmsi;` )


ts , latitude ve longitude istersek sorgu şu şekilde görünecektir:

 select last_row(ts, latitude, longitude) from vessel.ais_data partition by mmsi;


Bu tabloda yalnızca birkaç sütun olması çok önemli olmayabilir, ancak daha fazla tablo ve karmaşık sorgu olması uygulama performansında büyük bir fark yaratabilir.

Zaman Serisi işlevleri

TDEngine, performansı artırmak için kullanılması gereken bazı zaman serisine özgü uzantılara sahiptir. Örneğin, en son girişi almak için geleneksel bir SQL sorgusu:

 SELECT ts, name, latitude, longitude FROM vessel.ais_data order by ts desc limit 1;


Yürütülmesi 653 ms sürerken "TDEngine" sorgusu yalnızca 145 ms sürer:

 SELECT last_row(ts, name, latitude, longitude) FROM vessel.ais_data;


Last_row/first_row işlevleri ve diğer önbellek ayarları için optimize edilecek her tablo için yapılandırma seçenekleri vardır. TDEngine belgelerini okumanızı öneririm.

Çözüm

Basit versiyon: Bu makalede, bir TDEngine zaman serisi veritabanı kurduk ve istemci uygulamalarının verilere bağlanmasına ve verileri sorgulamasına olanak tanıyan bir GraphQL şeması tanımladık.


Daha pek çok şey var. Karmaşık zaman serisi verilerini ilişkisel verilerle şeffaf bir arayüzde birleştirmeye yönelik standart bir projemiz var. Nevados'ta birincil veritabanı olarak PostgreSQL'i kullanıyoruz ve zaman serisi verilerini yukarıdaki movement örneğinde olduğu gibi alıyoruz. Bu, birden çok kaynaktan gelen verileri tek bir API'de birleştirmenin harika bir yoludur. Diğer bir avantaj ise verilerin yalnızca istendiğinde getirilmesidir, bu da istemci uygulamasına büyük bir esneklik katar. Son fakat bir o kadar da önemlisi, GraphQL Şeması bir dokümantasyon ve sözleşme olarak çalışır, dolayısıyla "API Dokümantasyonu" kutusunu kolayca işaretleyebiliriz.


Herhangi bir sorunuz veya yorumunuz varsa lütfen BlueSky'ye ulaşın veya GitHub'daki tartışmaya katılın .


Burada da yayınlandı.