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
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ş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.
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.
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:
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 ...
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.
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:
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.
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;` ) } }) } })
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] } } })
🎉 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.
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!
Bu yazının kapsamını aşan birkaç şey var ama kısaca bahsetmek istiyorum:
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!
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.
Ö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.
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.
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ı.