Müşteriler ürününüzün içinde analitik bekliyorlar, ayrı bir BI aracı değil. ClickHouse, bunu büyük ölçekte gerçekleştirmek için en çok tercih edilen motordur. PostHog, LaunchDarkly ve Inigo gibi şirketler müşteri odaklı analitiklerini ClickHouse üzerinde çalıştırıyorlar.
Ancak hepsi aynı zorluğu keşfettiler: zor часть değil sorgu performansı, kiracı izolasyonu ve most online tavsiyelerin kritik detayları yanlış.
Doğru Şemayla Başlayın
Yüzlerce veya binlerce kiracısı olan bir SaaS için, paylaşılan olaylar tablosuna ihtiyacınız vardır. Bir tablo, tüm kiracılar, satır ilkesi ile izole edilmiştir. Bu, her ölçekteki ekibin kullandığı şeydir.
En önemli karar tenant_id nerede olduğudur: CREATE TABLE events ( tenant_id UInt32, event_id UUID, event_time DateTime64(3), user_id UInt64, event_type LowCardinality(String), properties Map(String, String) ) ENGINE = MergeTree() PARTITION BY toYYYYMM(event_time) ORDER BY (tenant_id, event_time, event_id);
tenant_id önce ORDER BY de olmalıdır. ClickHouse'ın seyrek birincil dizini sıralama anahtarına dayanır. Kiracı ilk olarak yerleştirildiğinde, her sorgu diğer kiracıların granüllerini tamamen atlar. Bunu yapmazsanız, her sorgu toplam tablo boyutuna orantılı olarak tarama yapar, kiracı verisi boyutuna değil.
Satır Düzeyinde Güvenlik: Ölçeklenen Desen
İki yaklaşım vardır. Ancak sadece biri gerçek bir SaaS için çalışır.
Naif yaklaşım, her kiracı için bir ClickHouse kullanıcısı oluşturur ve her kiracı için bir satır ilkesi oluşturur. Basit anlaşılabilir, ancak üretimde hemen bozulur çünkü bağlantı havuzunu bozar - her kiracı için ayrı bir havuza veya her istek için yeniden bağlanmaya ihtiyacınız vardır.
Üretim yaklaşımı, tek bir paylaşılan kullanıcı ile özel sorgu ayarları kullanır:
--Salt okunabilir role özel bir kiracı ayarı ekleyin CREATE ROLE analytics_readonly; ALTER ROLE analytics_readonly ADD SETTINGS SQL_tenant_id CHANGEABLE_IN_READONLY; GRANT SELECT ON events TO analytics_readonly; --Bir ilke, sorgu ayarını okur CREATE ROW POLICY tenant_isolation ON events FOR SELECT USING tenant_id = getSetting('SQL_tenant_id')::UInt32 TO analytics_readonly; --Paylaşılan bir uygulama kullanıcısı oluşturun CREATE USER app_analytics IDENTIFIED BY 'strong_password' SETTINGS readonly = 1; GRANT analytics_readonly TO app_analytics; SET DEFAULT ROLE analytics_readonly TO app_analytics;
Uygulamanız kiracı kimliğini sorgu başına geçirir:
await client.query({ query: SELECT event_type, count() AS total FROM events WHERE tenant_id = {tenantId:UInt32} AND event_time >= now() - INTERVAL 7 DAY GROUP BY event_type, query_params: { tenantId }, clickhouse_settings: { SQL_tenant_id: tenantId.toString(), quota_key: tenantId.toString(), }, });
Üç neden bu modelin doğru olması:
Kapalı thất bại. Uygulamanız SQL_tenant_id yi atlar ise, ClickHouse bir hata atar. Sessiz bir yol yok, kiracı bağlamı olmadan tüm kiracıların verilerini döndürür.
Bağlantı havuzu çalışır. Bir havuz, bir kullanıcı, tüm kiracılar. Kiracı kimliği, bağlantı düzeyinde değil, sorgu düzeyindedir.
Hiçbir DDL ek bir kiracı eklemek için gerekli değildir. 10.001. kiracıya ClickHouse CREATE USER veya CREATE POLICY gerekmez - sadece uygulamanızın kiracılar tablosunda bir satır.
Bir kritik detay neredeyse tüm öğreticiler tarafından kaçırılır: CHANGEABLE_IN_READONLY rolü gereklidir. Olmadan, salt okunabilir kullanıcılar özel ayarı ayarlayamaz ve her sorgu izin hata verir. Ayrıca, satır ilkesi doğru olmasını sağlar ancak birincil anahtar budama yapmaz - bu nedenle sorgularınızda WHERE tenant_id = ? yi açıkça dahil etmelisiniz.
TypeScript İle Bağlantı
Tam istek yolu: JWT → kiracı çıkar → ClickHouse sorgu ayarı → satır ilkesi gardiyanı.
Eğer çok sayıda analitik uç noktasını tanımlıyorsanız, bu enjeksiyonu merkezileştirmek nhanh sonuç verir. hypequery in serve API si kiracıyı bir kez context geri aramasında çıkarır ve her sorguya tip güvenli bir ctx nesnesi aracılığıyla geçirir:
import { createQueryBuilder } from '@hypequery/clickhouse';
import { initServe } from '@hypequery/serve';
import { z } from 'zod';
import type { IntrospectedSchema } from './generated-schema';
const db = createQueryBuilder<IntrospectedSchema>({host: process.env.CLICKHOUSE_HOST!, username: 'app_analytics', password: process.env.CLICKHOUSE_PASSWORD!, }); const { query, serve } = initServe({ context: ({ req }) => { const payload = verifyJWT(req.headers.get('authorization')); return { db, tenantId: payload.tenantId }; }, }); const eventCounts = query({ description: 'Son N gün için olay türlerine göre olay sayıları', input: z.object({ days: z.number().default(7) }), query: ({ ctx, input }) => ctx.db .table('events') .select(['event_type']) .count('event_id', 'total_events') .where('tenant_id', 'eq', ctx.tenantId) .where('event_time', 'gte', now() - INTERVAL ${input.days} DAY) .groupBy(['event_type']) .orderBy('total_events', 'DESC') .settings({ SQL_tenant_id: ctx.tenantId }) .execute(), }); export const api = serve({ queries: { eventCounts } });
Next.js App Router da monte edin:
// app/api/analytics/[[...slug]]/route.ts
import { createFetchHandler } from '@hypequery/serve';
import { api } from '@/analytics/api';
const handler = createFetchHandler(api.handler);
export { handler as GET, handler as POST };Hızlı Yapmak
Doğru ve izole olmak temel çizgidir. 100 ms nin altı gì hissettirir.
Toplu projeksiyonlar en yüksek kaldıraçlı optimizasyondur. Aynı parçaların içinde toplamları önceden hesaplar ve ClickHouse bunları yeniden hesaplamak yerine önceden hesaplanmış değerleri kullanır.
Gelecekte, çok kiracılı gösterge panellerini clickedHouse ile üretimde çalıştırmak için daha fazla rehberlik ve örnekler sunacağız.
Yapay zeka özeti
Müşteriler ürününüzde analitik bekliyorlar. ClickHouse ile çok kiracılı gösterge panelleri oluşturun ve müşteri verilerini güvenli bir şekilde izole edin.